Add PWMA implementation and tests; enhance documentation

This commit is contained in:
Miha Kralj
2025-12-13 20:21:21 -08:00
parent 60227a23c1
commit 4b17984cfd
38 changed files with 3506 additions and 25 deletions
+15
View File
@@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Trends", "quantower\Trends.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quantower.Tests", "quantower\Quantower.Tests.csproj", "{576835AB-6453-4413-A2E7-54B6725CDF9D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Momentum", "quantower\Momentum.csproj", "{4C83564F-433B-46EC-B6F4-1912F38D55A5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -73,6 +75,18 @@ Global
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Release|x64.Build.0 = Release|Any CPU
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Release|x86.ActiveCfg = Release|Any CPU
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Release|x86.Build.0 = Release|Any CPU
{4C83564F-433B-46EC-B6F4-1912F38D55A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4C83564F-433B-46EC-B6F4-1912F38D55A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4C83564F-433B-46EC-B6F4-1912F38D55A5}.Debug|x64.ActiveCfg = Debug|Any CPU
{4C83564F-433B-46EC-B6F4-1912F38D55A5}.Debug|x64.Build.0 = Debug|Any CPU
{4C83564F-433B-46EC-B6F4-1912F38D55A5}.Debug|x86.ActiveCfg = Debug|Any CPU
{4C83564F-433B-46EC-B6F4-1912F38D55A5}.Debug|x86.Build.0 = Debug|Any CPU
{4C83564F-433B-46EC-B6F4-1912F38D55A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4C83564F-433B-46EC-B6F4-1912F38D55A5}.Release|Any CPU.Build.0 = Release|Any CPU
{4C83564F-433B-46EC-B6F4-1912F38D55A5}.Release|x64.ActiveCfg = Release|Any CPU
{4C83564F-433B-46EC-B6F4-1912F38D55A5}.Release|x64.Build.0 = Release|Any CPU
{4C83564F-433B-46EC-B6F4-1912F38D55A5}.Release|x86.ActiveCfg = Release|Any CPU
{4C83564F-433B-46EC-B6F4-1912F38D55A5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -81,6 +95,7 @@ Global
{953F0406-DD9B-406E-993D-6D988D5F5423} = {3A8DF596-E814-FECC-DD4B-D8EF8AAC1A0D}
{D8F03B19-F99F-475F-8951-85C9D2258B73} = {6CF592EE-4302-E72F-3CB4-AB1D314DD5A8}
{576835AB-6453-4413-A2E7-54B6725CDF9D} = {6CF592EE-4302-E72F-3CB4-AB1D314DD5A8}
{4C83564F-433B-46EC-B6F4-1912F38D55A5} = {6CF592EE-4302-E72F-3CB4-AB1D314DD5A8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E6DB434C-508E-4231-B8A6-5EDD7FF87E22}
+283
View File
@@ -0,0 +1,283 @@
# QuanTAlib Indicators
| Indicator | Full Name | Category |
| :--- | :--- | :--- |
| ABBER | Aberration | Channels |
| AC | Acceleration Oscillator | Momentum |
| ACCBANDS | Acceleration Bands | Channels |
| ACCEL | Momentum change; 2nd derivative | Numerics |
| ADL | Accumulation/Distribution Line | Volume |
| ADOSC | Chaikin A/D Oscillator | Volume |
| ADR | Average Daily Range | Volatility |
| ADX | Average Directional Movement Index | Momentum |
| ADXR | Average Directional Movement Rating | Momentum |
| AFIRMA | Autoregressive FIR MA | Forecasts |
| ALLIGATOR | Williams Alligator | Trends |
| [ALMA](trends/alma/Alma.md) | Arnaud Legoux MA | Trends |
| AMAT | Archer Moving Averages Trends | Trends |
| AO | Awesome Oscillator | Momentum |
| AOBV | Archer On-Balance Volume | Volume |
| APCHANNEL | Andrews' Pitchfork | Channels |
| APO | Absolute Price Oscillator | Momentum |
| APZ | Adaptive Price Zone | Channels |
| AROON | Aroon | Momentum |
| AROONOSC | Aroon Oscillator | Momentum |
| ATAN2 | Two-Argument Arctangent | Numerics |
| ATR | Average True Range | Volatility |
| ATRBANDS | ATR Bands | Channels |
| ATRN | Average True Range Normalized [0,1] | Volatility |
| ATRP | Average True Range Percent | Volatility |
| BBANDS | Bollinger Bands | Channels |
| BBB | Bollinger %B | Momentum |
| BBS | Bollinger Band Squeeze | Momentum |
| BBW | Bollinger Band Width | Volatility |
| BBWN | Bollinger Band Width Normalized | Volatility |
| BBWP | Bollinger Band Width Percentile | Volatility |
| BESSEL | Bessel Filter | Trends |
| BETA | Beta Coefficient | Statistics |
| BIAS | Bias | Statistics |
| BILATERAL | Bilateral Filter | Trends |
| BLMA | Blackman Window MA | Trends |
| BOP | Balance of Power | Momentum |
| BPF | Ehlers Bandpass Filter | Trends |
| BUTTER | Butterworth Filter | Trends |
| BWMA | Bessel-Weighted MA | Trends |
| CCI | Commodity Channel Index | Momentum |
| CCV | Close-to-Close Volatility | Volatility |
| CFB | Jurik Composite Fractal Behavior | Cycles |
| CFO | Chande Forecast Oscillator | Forecasts |
| CG | Ehlers Center of Gravity | Cycles |
| CHANGE | Percentage Change | Numerics |
| CHEBY1 | Chebyshev Type I Filter | Trends |
| CHEBY2 | Chebyshev Type II Filter | Trends |
| CHOP | Choppiness Index | Momentum |
| CMF | Chaikin Money Flow | Volume |
| CMO | Chande Momentum Oscillator | Momentum |
| COINTEGRATION | Cointegration | Statistics |
| [CONV](trends/conv/Conv.md) | Convolution MA with any kernel | Trends |
| CORRELATION | Correlation (Pearson's) | Statistics |
| COVARIANCE | Covariance | Statistics |
| CUMMEAN | Cumulative Mean (Average) | Statistics |
| CV | Conditional Volatility | Volatility |
| CVI | Chaikin's Volatility | Volatility |
| DCHANNEL | Donchian Channels | Channels |
| DECAYCHANNEL | Decay Min-Max Channel | Channels |
| [DEMA](trends/dema/Dema.md) | Double Exponential MA | Trends |
| DIRTY | Dirty Data Detection | Errors |
| DMX | Jurik Directional Movement Index | Momentum |
| DPO | Detrended Price Oscillator | Momentum |
| DSMA | Deviation-Scaled MA | Trends |
| DSP | Detrended Synthetic Price | Cycles |
| [DWMA](trends/dwma/Dwma.md) | Double Weighted MA | Trends |
| DX | Directional Movement Index | Momentum |
| EACP | Ehlers Autocorrelation Periodogram | Cycles |
| EBSW | Ehlers Even Better Sinewave | Cycles |
| EFI | Elder's Force Index | Volume |
| ELLIPTIC | Elliptic (Cauer) Filter | Trends |
| [EMA](trends/ema/Ema.md) | Exponential MA | Trends |
| ENTROPY | Normalized Shannon Entropy | Statistics |
| EOME | Ease of Movement | Volume |
| EPMA | Endpoint MA | Trends |
| EWMA | Exponential Weighted MA Volatility | Volatility |
| EXP | Exponential Transformation | Numerics |
| FCB | Fractal Chaos Bands | Channels |
| FISHER | Ehlers Fisher Transform | Momentum |
| FRACTALS | Williams Fractals | Reversals |
| FRAMA | Ehlers Fractal Adaptive MA | Trends |
| GAUSS | Gaussian Filter | Trends |
| GEOMEAN | Geometric Mean | Statistics |
| GKV | Garman-Klass Volatility | Volatility |
| GRANGER | Granger Causality Test | Statistics |
| GWMA | Gaussian-Weighted MA | Trends |
| HAMMA | Hamming Window MA | Trends |
| HANMA | Hanning Window MA | Trends |
| HANN | Hann FIR Filter | Trends |
| HARMEAN | Harmonic Mean | Statistics |
| HEMA | Hull Exponential MA | Trends |
| HIGHEST | Highest value | Numerics |
| HL2 | (High + Low) / 2 | Numerics |
| HLC3 | (High + Low + Close) / 3 | Numerics |
| HLCC4 | (High + Low + Close + Close) / 4 | Numerics |
| HLV | High-Low Volatility | Volatility |
| [HMA](trends/hma/Hma.md) | Hull MA | Trends |
| HOMOD | Homodyne Discriminator Dominant Cycle | Cycles |
| HP | Hodrick-Prescott Filter | Trends |
| HPF | Ehlers Highpass Filter | Trends |
| HTIT | Ehlers Hilbert Transform Instantaneous Trend | Trends |
| HT_DCPERIOD | Ehlers Hilbert Transform Dominant Cycle Period | Cycles |
| HT_DCPHASE | Ehlers Hilbert Transform Dominant Cycle Phase | Cycles |
| HT_PHASOR | Ehlers Hilbert Transform Phasor Components | Cycles |
| HT_SINE | Ehlers Hilbert Transform SineWave | Cycles |
| HT_TRENDMODE | Ehlers Hilbert Transform Trend Mode | Trends |
| HUBER | Huber Loss | Errors |
| HURST | Hurst | Statistics |
| HV | Historical Volatility | Volatility |
| HWMA | Holt Weighted MA | Trends |
| ICHIMOKU | Ichimoku Cloud | Trends |
| III | Intraday Intensity Index | Volume |
| IMI | Intraday Momentum Index | Momentum |
| INERTIA | Inertia | Momentum |
| IQR | Interquartile Range | Statistics |
| JB | Jarque-Bera Test | Statistics |
| JBANDS | Jurik Volatility Bands | Channels |
| [JMA](trends/jma/Jma.md) | Jurik MA | Trends |
| JOLT | Rate of acceleration; 3rd derivative | Numerics |
| JVOLTY | Jurik Volatility | Volatility |
| JVOLTYN | Jurik Volatility Normalized [0,1] | Volatility |
| [KAMA](trends/kama/Kama.md) | Kaufman Adaptive MA | Trends |
| KCHANNEL | Keltner Channel | Channels |
| KDJ | KDJ Indicator | Momentum |
| KENDALL | Kendall Rank Correlation | Statistics |
| KF | Kalman Filter | Trends |
| KURTOSIS | Kurtosis | Statistics |
| KVO | Klinger Volume Oscillator | Volume |
| LINEAR | Linear Transformation | Numerics |
| LINREG | Linear Regression Curve | Statistics |
| LOESS | LOESS/LOWESS Smoothing | Trends |
| LOG | Logarithmic Transformation | Numerics |
| LOWEST | Lowest value | Numerics |
| [LSMA](trends/lsma/Lsma.md) | Least Squares Moving Average | Trends |
| LTMA | Linear Trend MA | Trends |
| LUNAR | Lunar Phase | Cycles |
| MACD | Moving Average Convergence Divergence | Momentum |
| MAE | Mean Absolute Error | Errors |
| MAENV | Moving Average Envelope | Channels |
| [MAMA](trends/mama/Mama.md) | Ehlers MESA Adaptive MA | Trends |
| MAPD | Mean Absolute Percentage Difference | Errors |
| MAPE | Mean Absolute Percentage Error | Errors |
| MASE | Mean Absolute Scaled Error | Errors |
| MASS | Mass Index | Volatility |
| ME | Mean Error | Errors |
| MEDIAN | Median (Statistical) | Statistics |
| MFI | Money Flow Index | Volume |
| MGDI | McGinley Dynamic Indicator | Trends |
| MIDPOINT | (Highest + Lowest) / 2 | Numerics |
| MMA | Modified MA | Trends |
| MMCHANNEL | Min-Max Channel | Channels |
| MODE | Mode (Most Frequent) | Statistics |
| MOM | Momentum | Momentum |
| MOON | Moon Phase | Cycles |
| MPE | Mean Percentage Error | Errors |
| MSE | Mean Squared Error | Errors |
| MSLE | Mean Squared Logarithmic Error | Errors |
| NATR | Normalized Average True Range | Volatility |
| NORMALIZE | Min-Max Scaling (Normalization) | Numerics |
| NOTCH | Notch Filter | Trends |
| NVI | Negative Volume Index | Volume |
| OBV | On Balance Volume | Volume |
| OC2 | (Open + Close) / 2 | Numerics |
| OHL3 | (Open + High + Low) / 3 | Numerics |
| OHLC4 | (Open + High + Low + Close) / 4 | Numerics |
| PCHANNEL | Price Channel | Channels |
| PERCENTILE | Percentile | Statistics |
| PGO | Pretty Good Oscillator | Momentum |
| PHASOR | Ehlers Phasor Analysis | Cycles |
| PIVOT | Pivot Points (Classic) | Reversals |
| PIVOTCAM | Camarilla Pivot Points | Reversals |
| PIVOTDEM | DeMark Pivot Points | Reversals |
| PIVOTEXT | Extended Traditional Pivots | Reversals |
| PIVOTFIB | Fibonacci Pivot Points | Reversals |
| PIVOTWOOD | Woodie's Pivot Points | Reversals |
| PMO | Price Momentum Oscillator | Momentum |
| PPO | Percentage Price Oscillator | Momentum |
| PRS | Price Relative Strength | Momentum |
| PSAR | Parabolic Stop And Reverse | Reversals |
| PV | Parkinson Volatility | Volatility |
| PVD | Price Volume Divergence | Volume |
| PVI | Positive Volume Index | Volume |
| PVO | Percentage Volume Oscillator | Volume |
| PVR | Price Volume Rank | Volume |
| PVT | Price Volume Trend | Volume |
| PWMA | Pascal Weighted MA | Trends |
| QEMA | Quadruple Exponential MA | Trends |
| QSTICK | Qstick Indicator | Momentum |
| QUANTILE | Quantile | Statistics |
| RAE | Relative Absolute Error | Errors |
| REGCHANNEL | Regression Channels | Channels |
| RELU | Rectified Linear Unit | Numerics |
| REMA | Regularized Exponential MA | Trends |
| RGMA | Recursive Gaussian MA | Trends |
| [RMA](trends/rma/Rma.md) | wildeR MA (SMMA, MMA) | Trends |
| RMSE | Root Mean Squared Error | Errors |
| RMSLE | Root Mean Squared Logarithmic Error | Errors |
| ROC | Rate of Change | Momentum |
| ROCP | Rate of Change Percentage | Momentum |
| ROCR | Rate of Change Ratio | Momentum |
| RSE | Relative Squared Error | Errors |
| RSI | Relative Strength Index | Momentum |
| RSQUARED | R-Squared | Errors |
| RSV | Rogers-Satchell Volatility | Volatility |
| RSX | Jurik Relative Strength Quality Index | Momentum |
| RV | Realized Volatility | Volatility |
| RVI | Relative Volatility Index | Volatility |
| SDCHANNEL | Standard Deviation Channel | Channels |
| SGF | Savitzky-Golay Filter | Trends |
| SGMA | Savitzky-Golay MA | Trends |
| SIGMOID | Logistic Function | Numerics |
| SINE | Ehlers Sine Wave | Cycles |
| SINEMA | Sine-weighted MA | Trends |
| SKEW | Skewness | Statistics |
| SLOPE | Rate of change; 1st derivative | Numerics |
| [SMA](trends/sma/Sma.md) | Simple MA | Trends |
| SMAPE | Symmetric Mean Absolute Percentage Error | Errors |
| SMI | Stochastic Momentum Index | Momentum |
| SOLAR | Solar Activity Cycle | Cycles |
| SPEARMAN | Spearman Rank Correlation | Statistics |
| SQRT | Square Root Transformation | Numerics |
| SSF | Ehlers Super Smooth Filter | Trends |
| SSFDSP | Ehlers SSF-Based Detrended Synthetic Price | Cycles |
| STANDARDIZE | Standardization (Z-score) | Numerics |
| STARC | Starc Bands | Volatility |
| STARCHANNEL | Stoller Average Range Channel | Channels |
| STBANDS | Super Trend Bands | Channels |
| STC | Schaff Trend Cycle | Cycles |
| STDDEV | Standard Deviation | Statistics |
| STOCH | Stochastic Oscillator | Momentum |
| STOCHF | Stochastic Fast | Momentum |
| STOCHRSI | Stochastic RSI | Momentum |
| SUPER | SuperTrend | Trends |
| SWINGS | Swing High/Low Detection | Reversals |
| [T3](trends/t3/T3.md) | Tillson T3 MA | Trends |
| TANH | Hyperbolic Tangent | Numerics |
| [TEMA](trends/tema/Tema.md) | Triple Exponential MA | Trends |
| THEIL | Theil Index | Statistics |
| TR | True Range | Volatility |
| [TRIMA](trends/trima/Trima.md) | Triangular MA | Trends |
| TRIX | Triple Exponential Average | Momentum |
| TSF | Time Series Forecast | Forecasts |
| TSI | True Strength Index | Momentum |
| TTM | TTM Trend | Trends |
| TVI | Trade Volume Index | Volume |
| TWAP | Time Weighted Average Price | Volume |
| UBANDS | Ultimate Bands | Channels |
| UCHANNEL | Ultimate Channel | Channels |
| UI | Ulcer Index | Volatility |
| ULTOSC | Ultimate Oscillator | Momentum |
| USF | Ehlers Ultrasmooth Filter | Trends |
| VA | Volume Accumulation | Volume |
| VAMA | Volatility Adjusted Moving Average | Trends |
| VARIANCE | Variance | Statistics |
| VEL | Jurik Velocity | Momentum |
| VF | Volume Force | Volume |
| [VIDYA](trends/vidya/Vidya.md) | Variable Index Dynamic Average | Trends |
| VO | Volume Oscillator | Volume |
| VORTEX | Vortex Indicator | Momentum |
| VOV | Volatility of Volatility | Volatility |
| VR | Volatility Ratio | Volatility |
| VROC | Volume Rate of Change | Volume |
| VWAD | Volume Weighted Accumulation/Distribution | Volume |
| VWAP | Volume Weighted Average Price | Volume |
| VWAPBANDS | VWAP Bands | Channels |
| VWAPSD | VWAP with Standard Deviation Bands | Channels |
| VWMA | Volume Weighted MA | Volume |
| WAD | Williams Accumulation/Distribution | Volume |
| WIENER | Wiener Filter | Trends |
| WILLR | Williams %R | Momentum |
| [WMA](trends/wma/Wma.md) | Weighted MA | Trends |
| YZV | Yang-Zhang Volatility | Volatility |
| YZVAMA | Yang-Zhang Volatility Adjusted MA | Trends |
| ZLDEMA | Zero-Lag Double Exponential MA | Trends |
| ZLEMA | Zero-Lag Exponential MA | Trends |
| ZLTEMA | Zero-Lag Triple Exponential MA | Trends |
| ZSCORE | Z-score standardization | Statistics |
| ZTEST | Z-Test | Statistics |
+28
View File
@@ -0,0 +1,28 @@
# Channels
Price channel and band overlays on the chart, typically derived from price extremes or volatility.
| Indicator | Full Name | Description |
| :--- | :--- | :--- |
| ABBER | Aberration | |
| ACCBANDS | Acceleration Bands | |
| APCHANNEL | Andrews' Pitchfork | |
| APZ | Adaptive Price Zone | |
| ATRBANDS | ATR Bands | |
| BBANDS | Bollinger Bands | |
| DCHANNEL | Donchian Channels | |
| DECAYCHANNEL | Decay Min-Max Channel | |
| FCB | Fractal Chaos Bands | |
| JBANDS | Jurik Volatility Bands | |
| KCHANNEL | Keltner Channel | |
| MAENV | Moving Average Envelope | |
| MMCHANNEL | Min-Max Channel | |
| PCHANNEL | Price Channel | |
| REGCHANNEL | Regression Channels | |
| SDCHANNEL | Standard Deviation Channel | |
| STARCHANNEL | Stoller Average Range Channel | |
| STBANDS | Super Trend Bands | |
| UBANDS | Ultimate Bands | |
| UCHANNEL | Ultimate Channel | |
| VWAPBANDS | VWAP Bands | |
| VWAPSD | VWAP with Standard Deviation Bands | |
+53
View File
@@ -472,6 +472,59 @@ public class SimdExtensionsTests
Assert.Equal(-1.0, max);
}
// Add/Subtract tests
[Fact]
public void Add_SameLength_CorrectResult()
{
double[] left = [1.0, 2.0, 3.0, 4.0, 5.0];
double[] right = [10.0, 20.0, 30.0, 40.0, 50.0];
double[] result = new double[5];
SimdExtensions.Add(left, right, result);
Assert.Equal(11.0, result[0]);
Assert.Equal(22.0, result[1]);
Assert.Equal(33.0, result[2]);
Assert.Equal(44.0, result[3]);
Assert.Equal(55.0, result[4]);
}
[Fact]
public void Add_DifferentLengths_ThrowsArgumentException()
{
double[] left = [1.0, 2.0];
double[] right = [1.0];
double[] result = new double[2];
Assert.Throws<ArgumentException>(() => SimdExtensions.Add(left, right, result));
}
[Fact]
public void Subtract_SameLength_CorrectResult()
{
double[] left = [10.0, 20.0, 30.0, 40.0, 50.0];
double[] right = [1.0, 2.0, 3.0, 4.0, 5.0];
double[] result = new double[5];
SimdExtensions.Subtract(left, right, result);
Assert.Equal(9.0, result[0]);
Assert.Equal(18.0, result[1]);
Assert.Equal(27.0, result[2]);
Assert.Equal(36.0, result[3]);
Assert.Equal(45.0, result[4]);
}
[Fact]
public void Subtract_DifferentLengths_ThrowsArgumentException()
{
double[] left = [1.0, 2.0];
double[] right = [1.0];
double[] result = new double[2];
Assert.Throws<ArgumentException>(() => SimdExtensions.Subtract(left, right, result));
}
// Integration tests
[Fact]
public void SIMD_WorksWithTSeriesValues()
+56
View File
@@ -381,6 +381,62 @@ public static class SimdExtensions
return MinMaxScalar(span);
}
/// <summary>
/// Element-wise addition of two spans using SIMD.
/// result[i] = left[i] + right[i]
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add(ReadOnlySpan<double> left, ReadOnlySpan<double> right, Span<double> result)
{
if (left.Length != right.Length || left.Length != result.Length)
throw new ArgumentException("All spans must have the same length");
int i = 0;
if (Vector.IsHardwareAccelerated && left.Length >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;
for (; i <= left.Length - vectorSize; i += vectorSize)
{
var vLeft = new Vector<double>(left.Slice(i, vectorSize));
var vRight = new Vector<double>(right.Slice(i, vectorSize));
(vLeft + vRight).CopyTo(result.Slice(i, vectorSize));
}
}
for (; i < left.Length; i++)
{
result[i] = left[i] + right[i];
}
}
/// <summary>
/// Element-wise subtraction of two spans using SIMD.
/// result[i] = left[i] - right[i]
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Subtract(ReadOnlySpan<double> left, ReadOnlySpan<double> right, Span<double> result)
{
if (left.Length != right.Length || left.Length != result.Length)
throw new ArgumentException("All spans must have the same length");
int i = 0;
if (Vector.IsHardwareAccelerated && left.Length >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;
for (; i <= left.Length - vectorSize; i += vectorSize)
{
var vLeft = new Vector<double>(left.Slice(i, vectorSize));
var vRight = new Vector<double>(right.Slice(i, vectorSize));
(vLeft - vRight).CopyTo(result.Slice(i, vectorSize));
}
}
for (; i < left.Length; i++)
{
result[i] = left[i] - right[i];
}
}
/// <summary>
/// Calculates the dot product of two spans using SIMD intrinsics.
/// Supports AVX512, AVX2, and NEON (ARM64).
+23
View File
@@ -0,0 +1,23 @@
# Cycles
Indicators focusing on cycle detection and periodicity in market data.
| Indicator | Full Name | Description |
| :--- | :--- | :--- |
| CFB | Jurik Composite Fractal Behavior | |
| CG | Ehlers Center of Gravity | |
| DSP | Detrended Synthetic Price | |
| EACP | Ehlers Autocorrelation Periodogram | |
| EBSW | Ehlers Even Better Sinewave | |
| HOMOD | Homodyne Discriminator Dominant Cycle | |
| HT_DCPERIOD | Ehlers Hilbert Transform Dominant Cycle Period | |
| HT_DCPHASE | Ehlers Hilbert Transform Dominant Cycle Phase | |
| HT_PHASOR | Ehlers Hilbert Transform Phasor Components | |
| HT_SINE | Ehlers Hilbert Transform SineWave | |
| LUNAR | Lunar Phase | |
| MOON | Moon Phase | |
| PHASOR | Ehlers Phasor Analysis | |
| SINE | Ehlers Sine Wave | |
| SOLAR | Solar Activity Cycle | |
| SSFDSP | Ehlers SSF-Based Detrended Synthetic Price | |
| STC | Schaff Trend Cycle | |
+22
View File
@@ -0,0 +1,22 @@
# Errors
Error metrics and performance indicators for model/strategy evaluation.
| Indicator | Full Name | Description |
| :--- | :--- | :--- |
| DIRTY | Dirty Data Detection | |
| HUBER | Huber Loss | |
| MAE | Mean Absolute Error | |
| MAPD | Mean Absolute Percentage Difference | |
| MAPE | Mean Absolute Percentage Error | |
| MASE | Mean Absolute Scaled Error | |
| ME | Mean Error | |
| MPE | Mean Percentage Error | |
| MSE | Mean Squared Error | |
| MSLE | Mean Squared Logarithmic Error | |
| RAE | Relative Absolute Error | |
| RMSE | Root Mean Squared Error | |
| RMSLE | Root Mean Squared Logarithmic Error | |
| RSE | Relative Squared Error | |
| RSQUARED | R-Squared | |
| SMAPE | Symmetric Mean Absolute Percentage Error | |
+9
View File
@@ -0,0 +1,9 @@
# Forecasts
Forecasting and predictive indicators/models.
| Indicator | Full Name | Description |
| :--- | :--- | :--- |
| AFIRMA | Autoregressive FIR MA | |
| CFO | Chande Forecast Oscillator | |
| TSF | Time Series Forecast | |
+48
View File
@@ -0,0 +1,48 @@
# Momentum
Momentum indicators measure the speed or strength of price movements. This includes classic oscillators and rate-of-change indicators.
| Indicator | Full Name | Description |
| :--- | :--- | :--- |
| AC | Acceleration Oscillator | |
| ADX | Average Directional Movement Index | |
| ADXR | Average Directional Movement Rating | |
| AO | Awesome Oscillator | |
| APO | Absolute Price Oscillator | |
| AROON | Aroon | |
| AROONOSC | Aroon Oscillator | |
| BBB | Bollinger %B | |
| BBS | Bollinger Band Squeeze | |
| BOP | Balance of Power | |
| CCI | Commodity Channel Index | |
| CHOP | Choppiness Index | |
| CMO | Chande Momentum Oscillator | |
| DMX | Jurik Directional Movement Index | |
| DPO | Detrended Price Oscillator | |
| DX | Directional Movement Index | |
| FISHER | Ehlers Fisher Transform | |
| IMI | Intraday Momentum Index | |
| INERTIA | Inertia | |
| KDJ | KDJ Indicator | |
| MACD | Moving Average Convergence Divergence | |
| MOM | Momentum | |
| PGO | Pretty Good Oscillator | |
| PMO | Price Momentum Oscillator | |
| PPO | Percentage Price Oscillator | |
| PRS | Price Relative Strength | |
| QSTICK | Qstick Indicator | |
| ROC | Rate of Change | |
| ROCP | Rate of Change Percentage | |
| ROCR | Rate of Change Ratio | |
| RSI | Relative Strength Index | |
| [RSX](rsx/Rsx.md) | Relative Strength X (Jurik's RSI Variant) | Noise-free, zero-lag version of RSI |
| SMI | Stochastic Momentum Index | |
| STOCH | Stochastic Oscillator | |
| STOCHF | Stochastic Fast | |
| STOCHRSI | Stochastic RSI | |
| TRIX | Triple Exponential Average | |
| TSI | True Strength Index | |
| ULTOSC | Ultimate Oscillator | |
| [VEL](vel/Vel.md) | Jurik Velocity | Momentum oscillator calculated as the difference between Parabolic Weighted MA and Weighted MA. |
| VORTEX | Vortex Indicator | |
| WILLR | Williams %R | |
+179
View File
@@ -0,0 +1,179 @@
using Xunit;
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class RsxIndicatorTests
{
[Fact]
public void RsxIndicator_Constructor_SetsDefaults()
{
var indicator = new RsxIndicator();
Assert.Equal(14, indicator.Period);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("RSX - Relative Strength X", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void RsxIndicator_MinHistoryDepths_EqualsPeriod()
{
var indicator = new RsxIndicator { Period = 20 };
Assert.Equal(20, indicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(20, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void RsxIndicator_ShortName_IncludesPeriodAndSource()
{
var indicator = new RsxIndicator { Period = 15 };
Assert.Contains("RSX", indicator.ShortName);
Assert.Contains("15", indicator.ShortName);
}
[Fact]
public void RsxIndicator_SourceCodeLink_IsValid()
{
var indicator = new RsxIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink);
Assert.Contains("Rsx.Quantower.cs", indicator.SourceCodeLink);
}
[Fact]
public void RsxIndicator_Initialize_CreatesInternalRsx()
{
var indicator = new RsxIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void RsxIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new RsxIndicator { 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 RsxIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new RsxIndicator { 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 RsxIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new RsxIndicator { 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 RsxIndicator_OnPaintChart_DoesNotThrow()
{
var indicator = new RsxIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(RsxIndicator), method.DeclaringType);
}
[Fact]
public void RsxIndicator_MultipleUpdates_ProducesCorrectRsxSequence()
{
var indicator = new RsxIndicator { 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 RsxIndicator_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 RsxIndicator { 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 RsxIndicator_Period_CanBeChanged()
{
var indicator = new RsxIndicator { Period = 5 };
Assert.Equal(5, indicator.Period);
indicator.Period = 20;
Assert.Equal(20, indicator.Period);
Assert.Equal(20, indicator.MinHistoryDepths);
}
}
+65
View File
@@ -0,0 +1,65 @@
using System.Drawing;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
public class RsxIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 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 Rsx? _rsx;
protected LineSeries? Series;
protected string? SourceName;
private int _warmupBarIndex = -1;
public int MinHistoryDepths => Period;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"RSX {Period}:{SourceName}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/momentum/rsx/Rsx.Quantower.cs";
public RsxIndicator()
{
OnBackGround = true;
SeparateWindow = true;
SourceName = Source.ToString();
Name = "RSX - Relative Strength X";
Description = "Jurik's RSX: A noise-free, zero-lag version of RSI";
Series = new(name: $"RSX {Period}", color: IndicatorExtensions.Momentum, width: 2, style: LineStyle.Solid);
AddLineSeries(Series);
}
protected override void OnInit()
{
_rsx = new Rsx(Period);
SourceName = Source.ToString();
_warmupBarIndex = -1;
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 = _rsx!.Update(input, isNew);
Series!.SetValue(result.Value);
Series!.SetMarker(0, Color.Transparent);
if (_warmupBarIndex < 0 && _rsx!.IsHot)
_warmupBarIndex = Count;
}
public override void OnPaintChart(PaintChartEventArgs args)
{
base.OnPaintChart(args);
int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count;
this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2);
}
}
+108
View File
@@ -0,0 +1,108 @@
using System;
using Xunit;
namespace QuanTAlib;
public class RsxTests
{
private readonly GBM _gbm;
public RsxTests()
{
_gbm = new GBM();
}
[Fact]
public void Constructor_InvalidPeriod_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Rsx(0));
Assert.Throws<ArgumentException>(() => new Rsx(-1));
}
[Fact]
public void Update_ValidInput_ReturnsValidRsx()
{
var rsx = new Rsx(14);
var result = rsx.Update(new TValue(DateTime.UtcNow, 100));
Assert.InRange(result.Value, 0, 100);
}
[Fact]
public void Update_NaN_UsesLastValidValue()
{
var rsx = new Rsx(14);
rsx.Update(new TValue(DateTime.UtcNow, 100));
var result = rsx.Update(new TValue(DateTime.UtcNow, double.NaN));
// Should not be NaN
Assert.False(double.IsNaN(result.Value));
Assert.InRange(result.Value, 0, 100);
}
[Fact]
public void Update_IsNew_Consistency()
{
var rsx = new Rsx(14);
var time = DateTime.UtcNow;
// Update with isNew=true
var val1 = rsx.Update(new TValue(time, 100), true);
// Update with isNew=false (same time, different value)
rsx.Update(new TValue(time, 105), false);
// Update with isNew=false (same time, original value) - should match val1 if state rollback works
// Note: RSX is highly sensitive to path, so exact match might be tricky if intermediate states drift,
// but for a single step rollback it should be very close.
var val3 = rsx.Update(new TValue(time, 100), false);
Assert.Equal(val1.Value, val3.Value, 1e-9);
}
[Fact]
public void Calculate_Span_Matches_Update()
{
int period = 14;
int count = 100;
var bars = _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var rsx = new Rsx(period);
var resultSeries = rsx.Update(series);
var spanInput = series.Values.ToArray();
var spanOutput = new double[count];
Rsx.Calculate(spanInput, spanOutput, period);
for (int i = 0; i < count; i++)
{
Assert.Equal(resultSeries.Values[i], spanOutput[i], 1e-9);
}
}
[Fact]
public void Reset_ClearsState()
{
var rsx = new Rsx(14);
rsx.Update(new TValue(DateTime.UtcNow, 100));
rsx.Reset();
// After reset, it should behave like a new instance
// RSX initializes with 0 filters.
// If we feed it the same value, it should produce the same initial output.
// However, RSX output depends on change (v8), so first value sets LastF8 but v8=0.
var val1 = rsx.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(50.0, val1.Value); // Neutral start
}
[Fact]
public void Chain_Works()
{
var rsx = new Rsx(14);
var rsx2 = new Rsx(rsx, 14);
var result = rsx2.Update(new TValue(DateTime.UtcNow, 100));
Assert.False(double.IsNaN(result.Value));
}
}
+126
View File
@@ -0,0 +1,126 @@
using System;
using Xunit;
namespace QuanTAlib;
public class RsxValidationTests
{
private readonly GBM _gbm;
public RsxValidationTests()
{
_gbm = new GBM();
}
[Fact]
public void Validate_Against_Reference_Implementation()
{
// Generate data
int count = 1000;
int period = 14;
var bars = _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var prices = bars.Close.Values;
// QuanTAlib implementation
var rsx = new Rsx(period);
var quantalibResults = new double[count];
for (int i = 0; i < count; i++)
{
quantalibResults[i] = rsx.Update(new TValue(DateTime.UtcNow, prices[i])).Value;
}
// Reference implementation (from user prompt)
var refRsx = new ReferenceRsx(period);
var refResults = new double[count];
for (int i = 0; i < count; i++)
{
refResults[i] = refRsx.Add(prices[i]);
}
// Compare
for (int i = 0; i < count; i++)
{
// Allow small difference due to floating point arithmetic order
Assert.Equal(refResults[i], quantalibResults[i], 1e-9);
}
}
// Reference implementation provided in the task description
private class ReferenceRsx
{
private readonly double alpha, ialpha;
// Internal state variables for filter registers:
private double f28, f30, f38, f40, f48, f50;
private double f58, f60, f68, f70, f78, f80;
// Added state for f10 logic
private double lastF8;
private bool initialized;
public double Current { get; private set; }
public ReferenceRsx(int length)
{
// Initialize constants:
this.alpha = 3.0 / (length + 2.0);
this.ialpha = 1.0 - this.alpha;
// Initialize filters to 0:
f28 = f30 = f38 = f40 = f48 = f50 = 0.0;
f58 = f60 = f68 = f70 = f78 = f80 = 0.0;
this.Current = 50.0; // neutral start
this.initialized = false;
}
public double Add(double price)
{
// Core RSX calculations (assuming price input as closing price):
double f8 = 100 * price;
if (!initialized)
{
lastF8 = f8;
initialized = true;
}
double v8 = f8 - lastF8;
lastF8 = f8;
// First smoothing stage:
f28 = ialpha * f28 + alpha * v8;
f30 = alpha * f28 + ialpha * f30;
double vC = 1.5 * f28 - 0.5 * f30;
// Second smoothing stage:
f38 = ialpha * f38 + alpha * vC;
f40 = alpha * f38 + ialpha * f40;
double v10 = 1.5 * f38 - 0.5 * f40;
// Third smoothing stage:
f48 = ialpha * f48 + alpha * v10;
f50 = alpha * f48 + ialpha * f50;
double v14 = 1.5 * f48 - 0.5 * f50;
// Repeat stages for absolute value (momentum magnitude):
f58 = ialpha * f58 + alpha * Math.Abs(v8);
f60 = alpha * f58 + ialpha * f60;
double v18 = 1.5 * f58 - 0.5 * f60;
f68 = ialpha * f68 + alpha * v18;
f70 = alpha * f68 + ialpha * f70;
double v1C = 1.5 * f68 - 0.5 * f70;
f78 = ialpha * f78 + alpha * v1C;
f80 = alpha * f78 + ialpha * f80;
double v20 = 1.5 * f78 - 0.5 * f80;
// Final RSX value:
double rsx;
if (v20 > 1e-10) // Avoid division by zero
{
double v4 = (v14 / v20 + 1.0) * 50.0;
rsx = Math.Clamp(v4, 0.0, 100.0);
}
else
{
rsx = 50.0;
}
this.Current = rsx;
return rsx;
}
}
}
+306
View File
@@ -0,0 +1,306 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// RSX: Relative Strength X (Jurik's RSI Variant)
/// </summary>
/// <remarks>
/// RSX is a noise-free version of RSI that eliminates lag and choppiness.
/// It uses a cascading IIR filter structure to achieve smoothness while preserving
/// turning points and the 0-100 range.
///
/// Key characteristics:
/// - Zero lag (compared to smoothed RSI)
/// - Ultra smooth output
/// - Bounded 0-100
///
/// Sources:
/// - https://scribd.com/document/253633684/Jurik-RSX
/// - https://www.prorealcode.com/prorealtime-indicators/jurik-rsx/
/// </remarks>
[SkipLocalsInit]
public sealed class Rsx : ITValuePublisher
{
private readonly int _period;
private readonly double _alpha;
private record struct State
{
// Momentum filters (3 stages, 2 filters each)
public double M1_1, M1_2;
public double M2_1, M2_2;
public double M3_1, M3_2;
// Absolute Momentum filters (3 stages, 2 filters each)
public double A1_1, A1_2;
public double A2_1, A2_2;
public double A3_1, A3_2;
public double LastPrice;
public double LastValidValue;
public bool IsInitialized;
}
private State _state;
private State _p_state;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
/// <summary>
/// Creates RSX with specified period.
/// </summary>
/// <param name="period">Length of the filter (typically 8-40).</param>
public Rsx(int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
_period = period;
_alpha = 3.0 / (period + 2.0);
Name = $"Rsx({period})";
}
public Rsx(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
}
/// <summary>
/// Current RSX value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the indicator has processed enough data to be considered valid.
/// </summary>
public bool IsHot => _state.IsInitialized;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double price = input.Value;
if (!double.IsFinite(price))
{
price = _state.LastValidValue;
}
else
{
_state.LastValidValue = price;
}
if (!_state.IsInitialized)
{
_state.LastPrice = price;
_state.IsInitialized = true;
}
// Calculate momentum (change in price * 100)
double momentum = (price - _state.LastPrice) * 100.0;
if (isNew)
{
_state.LastPrice = price;
}
// --- Momentum Smoothing ---
// Stage 1
_state.M1_1 += _alpha * (momentum - _state.M1_1);
_state.M1_2 += _alpha * (_state.M1_1 - _state.M1_2);
double m1_out = (3.0 * _state.M1_1 - _state.M1_2) * 0.5;
// Stage 2
_state.M2_1 += _alpha * (m1_out - _state.M2_1);
_state.M2_2 += _alpha * (_state.M2_1 - _state.M2_2);
double m2_out = (3.0 * _state.M2_1 - _state.M2_2) * 0.5;
// Stage 3
_state.M3_1 += _alpha * (m2_out - _state.M3_1);
_state.M3_2 += _alpha * (_state.M3_1 - _state.M3_2);
double smoothedMomentum = (3.0 * _state.M3_1 - _state.M3_2) * 0.5;
// --- Absolute Momentum Smoothing ---
double absMomentum = Math.Abs(momentum);
// Stage 1
_state.A1_1 += _alpha * (absMomentum - _state.A1_1);
_state.A1_2 += _alpha * (_state.A1_1 - _state.A1_2);
double a1_out = (3.0 * _state.A1_1 - _state.A1_2) * 0.5;
// Stage 2
_state.A2_1 += _alpha * (a1_out - _state.A2_1);
_state.A2_2 += _alpha * (_state.A2_1 - _state.A2_2);
double a2_out = (3.0 * _state.A2_1 - _state.A2_2) * 0.5;
// Stage 3
_state.A3_1 += _alpha * (a2_out - _state.A3_1);
_state.A3_2 += _alpha * (_state.A3_1 - _state.A3_2);
double smoothedAbsMomentum = (3.0 * _state.A3_1 - _state.A3_2) * 0.5;
// --- Final RSX Calculation ---
double rsx;
if (smoothedAbsMomentum > 1e-10)
{
double v4 = (smoothedMomentum / smoothedAbsMomentum + 1.0) * 50.0;
rsx = Math.Clamp(v4, 0.0, 100.0);
}
else
{
rsx = 50.0;
}
Last = new TValue(input.Time, rsx);
Pub?.Invoke(Last);
return Last;
}
public TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
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 by replaying the last few bars
Reset();
int warmup = Math.Max(0, len - 200);
for (int i = warmup; i < len; i++)
{
Update(new TValue(source.Times[i], source.Values[i]), true);
}
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, int period)
{
var rsx = new Rsx(period);
return rsx.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;
double alpha = 3.0 / (period + 2.0);
// Momentum filters
double m1_1 = 0, m1_2 = 0;
double m2_1 = 0, m2_2 = 0;
double m3_1 = 0, m3_2 = 0;
// Abs Momentum filters
double a1_1 = 0, a1_2 = 0;
double a2_1 = 0, a2_2 = 0;
double a3_1 = 0, a3_2 = 0;
double lastPrice = 0;
bool initialized = false;
double lastValidValue = 0;
for (int i = 0; i < len; i++)
{
double price = source[i];
if (!double.IsFinite(price))
{
price = lastValidValue;
}
else
{
lastValidValue = price;
}
if (!initialized)
{
lastPrice = price;
initialized = true;
}
double momentum = (price - lastPrice) * 100.0;
lastPrice = price;
// Momentum Smoothing
m1_1 += alpha * (momentum - m1_1);
m1_2 += alpha * (m1_1 - m1_2);
double m1_out = (3.0 * m1_1 - m1_2) * 0.5;
m2_1 += alpha * (m1_out - m2_1);
m2_2 += alpha * (m2_1 - m2_2);
double m2_out = (3.0 * m2_1 - m2_2) * 0.5;
m3_1 += alpha * (m2_out - m3_1);
m3_2 += alpha * (m3_1 - m3_2);
double smoothedMomentum = (3.0 * m3_1 - m3_2) * 0.5;
// Abs Momentum Smoothing
double absMomentum = Math.Abs(momentum);
a1_1 += alpha * (absMomentum - a1_1);
a1_2 += alpha * (a1_1 - a1_2);
double a1_out = (3.0 * a1_1 - a1_2) * 0.5;
a2_1 += alpha * (a1_out - a2_1);
a2_2 += alpha * (a2_1 - a2_2);
double a2_out = (3.0 * a2_1 - a2_2) * 0.5;
a3_1 += alpha * (a2_out - a3_1);
a3_2 += alpha * (a3_1 - a3_2);
double smoothedAbsMomentum = (3.0 * a3_1 - a3_2) * 0.5;
// Final RSX
double rsx;
if (smoothedAbsMomentum > 1e-10)
{
double v4 = (smoothedMomentum / smoothedAbsMomentum + 1.0) * 50.0;
rsx = Math.Clamp(v4, 0.0, 100.0);
}
else
{
rsx = 50.0;
}
output[i] = rsx;
}
}
public void Reset()
{
_state = default;
_p_state = default;
Last = default;
}
}
+68
View File
@@ -0,0 +1,68 @@
# RSX - Relative Strength X (Jurik's RSI Variant)
RSX is a noise-free version of the Relative Strength Index (RSI) developed by Mark Jurik. It eliminates the lag and choppiness associated with standard RSI and its smoothed variants. RSX preserves the 0-100 bounded range and turning points of RSI but provides a much smoother signal, making it easier to identify trends and reversals without false signals from whipsaw movements.
## Core Concepts
- **Zero Lag:** Uses a specialized IIR filter chain to smooth the data without introducing significant delay.
- **Noise Reduction:** Filters out high-frequency noise while retaining the underlying trend.
- **Bounded Range:** Output is strictly bounded between 0 and 100, similar to RSI.
- **Smoothness:** Produces a clean, continuous curve suitable for precise peak/valley detection.
## Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| Period | int | 14 | The smoothing period (typically 8-40). |
## Formula
RSX uses a cascading filter structure. The smoothing factor $\alpha$ is derived from the period:
$$ \alpha = \frac{3}{Period + 2} $$
The algorithm processes price changes ($v_8$) through multiple smoothing stages for both the raw momentum and its absolute value. The final RSX is calculated as:
$$ RSX = \left( \frac{v_{14}}{v_{20}} + 1 \right) \times 50 $$
Where $v_{14}$ is the smoothed momentum and $v_{20}$ is the smoothed absolute momentum.
## C# Implementation
### Standard Usage
```csharp
using QuanTAlib;
var rsx = new Rsx(14);
var result = rsx.Update(new TValue(DateTime.UtcNow, price));
Console.WriteLine($"RSX: {result.Value}");
```
### Span API (High Performance)
```csharp
double[] prices = { ... };
double[] results = new double[prices.Length];
Rsx.Calculate(prices, results, 14);
```
### Chaining
```csharp
var rsx = new Rsx(14);
var sma = new Sma(rsx, 3); // Smooth the RSX further
```
## Interpretation
- **Overbought/Oversold:** Values above 70 (or 80) indicate overbought conditions, while values below 30 (or 20) indicate oversold conditions.
- **Trend Confirmation:** RSX crossing 50 can signal a trend change.
- **Divergence:** Divergence between price and RSX often precedes a reversal.
- **Smoothness:** Due to its smoothness, RSX slope changes are more significant than RSI slope changes.
## References
- [Jurik Research](http://www.jurikres.com/)
- [ProRealCode - Jurik RSX](https://www.prorealcode.com/prorealtime-indicators/jurik-rsx/)
+179
View File
@@ -0,0 +1,179 @@
using Xunit;
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class VelIndicatorTests
{
[Fact]
public void VelIndicator_Constructor_SetsDefaults()
{
var indicator = new VelIndicator();
Assert.Equal(14, indicator.Period);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("VEL - Jurik's Velocity", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void VelIndicator_MinHistoryDepths_EqualsPeriod()
{
var indicator = new VelIndicator { Period = 20 };
Assert.Equal(20, indicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(20, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void VelIndicator_ShortName_IncludesPeriodAndSource()
{
var indicator = new VelIndicator { Period = 15 };
Assert.Contains("VEL", indicator.ShortName);
Assert.Contains("15", indicator.ShortName);
}
[Fact]
public void VelIndicator_SourceCodeLink_IsValid()
{
var indicator = new VelIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink);
Assert.Contains("Vel.Quantower.cs", indicator.SourceCodeLink);
}
[Fact]
public void VelIndicator_Initialize_CreatesInternalVel()
{
var indicator = new VelIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void VelIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new VelIndicator { 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 VelIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new VelIndicator { 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 VelIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new VelIndicator { 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 VelIndicator_OnPaintChart_DoesNotThrow()
{
var indicator = new VelIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(VelIndicator), method.DeclaringType);
}
[Fact]
public void VelIndicator_MultipleUpdates_ProducesCorrectVelSequence()
{
var indicator = new VelIndicator { 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 VelIndicator_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 VelIndicator { 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 VelIndicator_Period_CanBeChanged()
{
var indicator = new VelIndicator { Period = 5 };
Assert.Equal(5, indicator.Period);
indicator.Period = 20;
Assert.Equal(20, indicator.Period);
Assert.Equal(20, indicator.MinHistoryDepths);
}
}
+63
View File
@@ -0,0 +1,63 @@
using System.Drawing;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
public class VelIndicator : 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 Vel? _vel;
private int _warmupBarIndex = -1;
protected LineSeries? Series;
protected string? SourceName;
public int MinHistoryDepths => Period;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"VEL {Period}:{SourceName}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/momentum/vel/Vel.Quantower.cs";
public VelIndicator()
{
OnBackGround = true;
SeparateWindow = true;
SourceName = Source.ToString();
Name = "VEL - Jurik's Velocity";
Description = "Momentum oscillator calculated as PWMA - WMA";
Series = new(name: $"VEL {Period}", color: IndicatorExtensions.Momentum, width: 2, style: LineStyle.Solid);
AddLineSeries(Series);
}
protected override void OnInit()
{
_vel = new Vel(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 = _vel!.Update(input, isNew);
if (_warmupBarIndex < 0 && _vel!.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);
}
}
+210
View File
@@ -0,0 +1,210 @@
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class VelTests
{
[Fact]
public void Vel_Constructor_ValidatesInput()
{
Assert.Throws<ArgumentException>(() => new Vel(0));
Assert.Throws<ArgumentException>(() => new Vel(-1));
var vel = new Vel(10);
Assert.NotNull(vel);
}
[Fact]
public void Vel_Calc_ReturnsValue()
{
var vel = new Vel(10);
Assert.Equal(0, vel.Last.Value);
TValue result = vel.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(result.Value, vel.Last.Value);
}
[Fact]
public void Vel_Calc_IsNew_AcceptsParameter()
{
var vel = new Vel(10);
vel.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
double value1 = vel.Last.Value;
vel.Update(new TValue(DateTime.UtcNow, 200), isNew: true);
double value2 = vel.Last.Value;
// Values should change with new bars
Assert.NotEqual(value1, value2);
}
[Fact]
public void Vel_Calc_IsNew_False_UpdatesValue()
{
var vel = new Vel(10);
vel.Update(new TValue(DateTime.UtcNow, 100));
vel.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
double beforeUpdate = vel.Last.Value;
vel.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
double afterUpdate = vel.Last.Value;
// Update should change the value
Assert.NotEqual(beforeUpdate, afterUpdate);
}
[Fact]
public void Vel_Reset_ClearsState()
{
var vel = new Vel(10);
vel.Update(new TValue(DateTime.UtcNow, 100));
vel.Update(new TValue(DateTime.UtcNow, 105));
double valueBefore = vel.Last.Value;
vel.Reset();
Assert.Equal(0, vel.Last.Value);
// After reset, should accept new values
vel.Update(new TValue(DateTime.UtcNow, 50));
// First value is 0 because PWMA(50) = 50 and WMA(50) = 50
Assert.Equal(0, vel.Last.Value);
vel.Update(new TValue(DateTime.UtcNow, 60));
Assert.NotEqual(0, vel.Last.Value);
Assert.NotEqual(valueBefore, vel.Last.Value);
}
[Fact]
public void Vel_IsHot_BecomesTrueWhenBufferFull()
{
var vel = new Vel(5);
Assert.False(vel.IsHot);
for (int i = 1; i <= 4; i++)
{
vel.Update(new TValue(DateTime.UtcNow, i * 10));
Assert.False(vel.IsHot);
}
vel.Update(new TValue(DateTime.UtcNow, 50));
Assert.True(vel.IsHot);
}
[Fact]
public void Vel_CalculatesCorrectValue()
{
var vel = new Vel(3);
vel.Update(new TValue(DateTime.UtcNow, 10));
vel.Update(new TValue(DateTime.UtcNow, 20));
vel.Update(new TValue(DateTime.UtcNow, 30));
// PWMA(3) of 10,20,30 = 360/14 = 25.7142857...
// WMA(3) of 10,20,30 = 140/6 = 23.3333333...
// VEL = PWMA - WMA = 2.38095238...
double expectedPwma = 360.0 / 14.0;
double expectedWma = 140.0 / 6.0;
double expectedVel = expectedPwma - expectedWma;
Assert.Equal(expectedVel, vel.Last.Value, 1e-10);
}
[Fact]
public void Vel_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 = Vel.Calculate(series, 3);
Assert.Equal(3, results.Count);
double expectedPwma = 360.0 / 14.0;
double expectedWma = 140.0 / 6.0;
double expectedVel = expectedPwma - expectedWma;
Assert.Equal(expectedVel, results.Last.Value, 1e-10);
}
[Fact]
public void Vel_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 = Vel.Calculate(series, 10);
// Calculate with Span API
Vel.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 Vel_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 = Vel.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];
Vel.Calculate(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Vel(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 Vel(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);
}
}
+33
View File
@@ -0,0 +1,33 @@
using System;
using Xunit;
namespace QuanTAlib.Tests;
public class VelValidationTests
{
[Fact]
public void Vel_Matches_PwmaMinusWma()
{
// VEL = PWMA - WMA
// We validate this relationship holds true for a random sequence of data.
int period = 10;
var vel = new Vel(period);
var pwma = new Pwma(period);
var wma = new Wma(period);
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);
var input = new TValue(bar.Time, bar.Close);
var v = vel.Update(input);
var p = pwma.Update(input);
var w = wma.Update(input);
Assert.Equal(p.Value - w.Value, v.Value, 1e-10);
}
}
}
+106
View File
@@ -0,0 +1,106 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// VEL: Jurik's Velocity
/// </summary>
/// <remarks>
/// VEL is a momentum oscillator calculated as the difference between a Parabolic Weighted Moving Average (PWMA)
/// and a Weighted Moving Average (WMA) of the same period.
///
/// Calculation:
/// VEL = PWMA(Period) - WMA(Period)
///
/// This indicator measures the rate of change of the price, smoothed by the difference in weighting schemes.
/// </remarks>
[SkipLocalsInit]
public sealed class Vel : ITValuePublisher
{
private readonly Pwma _pwma;
private readonly Wma _wma;
public string Name { get; }
public TValue Last { get; private set; }
public bool IsHot => _pwma.IsHot && _wma.IsHot;
public event Action<TValue>? Pub;
public Vel(int period)
{
if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period));
_pwma = new Pwma(period);
_wma = new Wma(period);
Name = $"Vel({period})";
}
public Vel(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
var pwma = _pwma.Update(input, isNew);
var wma = _wma.Update(input, isNew);
Last = new TValue(input.Time, pwma.Value - wma.Value);
Pub?.Invoke(Last);
return Last;
}
public TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
// Update internal indicators to ensure their state is correct
var pwmaSeries = _pwma.Update(source);
var wmaSeries = _wma.Update(source);
// Calculate VEL series
int len = source.Count;
List<long> t = new(len);
List<double> v = new(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var vSpan = CollectionsMarshal.AsSpan(v);
SimdExtensions.Subtract(pwmaSeries.Values, wmaSeries.Values, vSpan);
source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
Last = new TValue(t[len - 1], v[len - 1]);
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, int period)
{
var vel = new Vel(period);
return vel.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");
Span<double> pwma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length];
Span<double> wma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length];
Pwma.Calculate(source, pwma, period);
Wma.Calculate(source, wma, period);
SimdExtensions.Subtract(pwma, wma, output);
}
public void Reset()
{
_pwma.Reset();
_wma.Reset();
Last = default;
}
}
+66
View File
@@ -0,0 +1,66 @@
# VEL - Jurik's Velocity
VEL (Jurik's Velocity) is a momentum oscillator that measures the rate of change of price. It is calculated as the difference between a Parabolic Weighted Moving Average (PWMA) and a Weighted Moving Average (WMA) of the same period.
## Core Concepts
- **Momentum:** Measures the speed of price movement.
- **Smoothing:** Uses moving averages to reduce noise compared to raw ROC (Rate of Change).
- **Parabolic vs Linear:** By subtracting a linear weighted average from a parabolic weighted average, VEL isolates the acceleration component of the price movement.
## Formula
$$
VEL_t = PWMA_t(n) - WMA_t(n)
$$
Where:
- $n$ is the period.
- $PWMA_t(n)$ is the Parabolic Weighted Moving Average.
- $WMA_t(n)$ is the Weighted Moving Average.
## Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| Period | int | - | The number of data points used in the calculation. Must be >= 1. |
## Usage
### Standard Usage
```csharp
using QuanTAlib;
var vel = new Vel(14);
var result = vel.Update(new TValue(DateTime.UtcNow, 100.0));
Console.WriteLine($"VEL: {result.Value}");
```
### Chaining
```csharp
var source = new Sma(10);
var vel = new Vel(source, 14);
```
### Batch Calculation (Span)
For high-performance scenarios, use the static `Calculate` method with `Span<double>`.
```csharp
double[] prices = { ... };
double[] results = new double[prices.Length];
Vel.Calculate(prices, results, 14);
```
## Interpretation
- **Zero Line Crossovers:** Crossing above zero indicates increasing upward momentum (acceleration). Crossing below zero indicates increasing downward momentum (deceleration).
- **Divergence:** Divergence between price and VEL can signal potential reversals.
- **Extremes:** High positive or negative values indicate strong momentum, which might precede a reversal or consolidation.
## References
- Jurik Research
+29
View File
@@ -0,0 +1,29 @@
# Numerics
Basic mathematical transforms and utility functions for time series.
| Indicator | Full Name | Description |
| :--- | :--- | :--- |
| ACCEL | Momentum change; 2nd derivative | |
| ATAN2 | Two-Argument Arctangent | |
| CHANGE | Percentage Change | |
| EXP | Exponential Transformation | |
| HIGHEST | Highest value | |
| HL2 | (High + Low) / 2 | |
| HLC3 | (High + Low + Close) / 3 | |
| HLCC4 | (High + Low + Close + Close) / 4 | |
| JOLT | Rate of acceleration; 3rd derivative | |
| LINEAR | Linear Transformation | |
| LOG | Logarithmic Transformation | |
| LOWEST | Lowest value | |
| MIDPOINT | (Highest + Lowest) / 2 | |
| NORMALIZE | Min-Max Scaling (Normalization) | |
| OC2 | (Open + Close) / 2 | |
| OHL3 | (Open + High + Low) / 3 | |
| OHLC4 | (Open + High + Low + Close) / 4 | |
| RELU | Rectified Linear Unit | |
| SIGMOID | Logistic Function | |
| SLOPE | Rate of change; 1st derivative | |
| SQRT | Square Root Transformation | |
| STANDARDIZE | Standardization (Z-score) | |
| TANH | Hyperbolic Tangent | |
+15
View File
@@ -0,0 +1,15 @@
# Reversals
Tools indicating potential reversals, support/resistance, or pivot points.
| Indicator | Full Name | Description |
| :--- | :--- | :--- |
| FRACTALS | Williams Fractals | |
| PIVOT | Pivot Points (Classic) | |
| PIVOTCAM | Camarilla Pivot Points | |
| PIVOTDEM | DeMark Pivot Points | |
| PIVOTEXT | Extended Traditional Pivots | |
| PIVOTFIB | Fibonacci Pivot Points | |
| PIVOTWOOD | Woodie's Pivot Points | |
| PSAR | Parabolic Stop And Reverse | |
| SWINGS | Swing High/Low Detection | |
+33
View File
@@ -0,0 +1,33 @@
# Statistics
Statistical analysis tools applied to price/returns.
| Indicator | Full Name | Description |
| :--- | :--- | :--- |
| BETA | Beta Coefficient | |
| BIAS | Bias | |
| COINTEGRATION | Cointegration | |
| CORRELATION | Correlation (Pearson's) | |
| COVARIANCE | Covariance | |
| CUMMEAN | Cumulative Mean (Average) | |
| ENTROPY | Normalized Shannon Entropy | |
| GEOMEAN | Geometric Mean | |
| GRANGER | Granger Causality Test | |
| HARMEAN | Harmonic Mean | |
| HURST | Hurst | |
| IQR | Interquartile Range | |
| JB | Jarque-Bera Test | |
| KENDALL | Kendall Rank Correlation | |
| KURTOSIS | Kurtosis | |
| LINREG | Linear Regression Curve | |
| MEDIAN | Median (Statistical) | |
| MODE | Mode (Most Frequent) | |
| PERCENTILE | Percentile | |
| QUANTILE | Quantile | |
| SKEW | Skewness | |
| SPEARMAN | Spearman Rank Correlation | |
| STDDEV | Standard Deviation | |
| THEIL | Theil Index | |
| VARIANCE | Variance | |
| ZSCORE | Z-score standardization | |
| ZTEST | Z-Test | |
+9
View File
@@ -1,3 +1,5 @@
# To do
| **Indicator Name** | **Libraries** |
| ---------------------------------------------------------------------------- | --------------------------------------------- |
| **Aroon** measures trend strength (Aroon Up/Down) | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
@@ -140,3 +142,10 @@
| **Schaff Trend Cycle** *(also listed in 3-library category)* | |
| **ConnorsRSI** *(if PineScript not included; otherwise 2 libraries)* | |
## Missing Indicators (Identified Gap Analysis)
- **Trends:** FWMA, GANN_HILO, MAVP, SSL, SWMA, PARA_WMA.
- **Momentum:** COPPOCK, ERI, KST, PSL, QQE, RMI, RVGI, VHF.
- **Volume:** BWMFI, VFI.
- **Volatility:** THERMO.
- **Statistics:** LINREG_ANGLE, LINREG_INT, LINREG_SLOPE, MAD.
+28 -23
View File
@@ -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 | |
+179
View File
@@ -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);
}
}
+63
View File
@@ -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);
}
}
+398
View File
@@ -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);
}
}
+54
View File
@@ -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");
}
}
+320
View File
@@ -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;
}
}
+227
View File
@@ -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
+1 -1
View File
@@ -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`)
+1 -1
View File
@@ -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`)
+34
View File
@@ -0,0 +1,34 @@
# Volatility
Volatility indicators measure price volatility and range.
| Indicator | Full Name | Description |
| :--- | :--- | :--- |
| ADR | Average Daily Range | |
| ATR | Average True Range | |
| ATRN | Average True Range Normalized [0,1] | |
| ATRP | Average True Range Percent | |
| BBW | Bollinger Band Width | |
| BBWN | Bollinger Band Width Normalized | |
| BBWP | Bollinger Band Width Percentile | |
| CCV | Close-to-Close Volatility | |
| CV | Conditional Volatility | |
| CVI | Chaikin's Volatility | |
| EWMA | Exponential Weighted MA Volatility | |
| GKV | Garman-Klass Volatility | |
| HLV | High-Low Volatility | |
| HV | Historical Volatility | |
| JVOLTY | Jurik Volatility | |
| JVOLTYN | Jurik Volatility Normalized [0,1] | |
| MASS | Mass Index | |
| NATR | Normalized Average True Range | |
| PV | Parkinson Volatility | |
| RSV | Rogers-Satchell Volatility | |
| RV | Realized Volatility | |
| RVI | Relative Volatility Index | |
| STARC | Starc Bands | |
| TR | True Range | |
| UI | Ulcer Index | |
| VOV | Volatility of Volatility | |
| VR | Volatility Ratio | |
| YZV | Yang-Zhang Volatility | |
+32
View File
@@ -0,0 +1,32 @@
# Volume
Volume indicators are based on trading volume and flow of funds.
| Indicator | Full Name | Description |
| :--- | :--- | :--- |
| ADL | Accumulation/Distribution Line | |
| ADOSC | Chaikin A/D Oscillator | |
| AOBV | Archer On-Balance Volume | |
| CMF | Chaikin Money Flow | |
| EFI | Elder's Force Index | |
| EOME | Ease of Movement | |
| III | Intraday Intensity Index | |
| KVO | Klinger Volume Oscillator | |
| MFI | Money Flow Index | |
| NVI | Negative Volume Index | |
| OBV | On Balance Volume | |
| PVD | Price Volume Divergence | |
| PVI | Positive Volume Index | |
| PVO | Percentage Volume Oscillator | |
| PVR | Price Volume Rank | |
| PVT | Price Volume Trend | |
| TVI | Trade Volume Index | |
| TWAP | Time Weighted Average Price | |
| VA | Volume Accumulation | |
| VF | Volume Force | |
| VO | Volume Oscillator | |
| VROC | Volume Rate of Change | |
| VWAD | Volume Weighted Accumulation/Distribution | |
| VWAP | Volume Weighted Average Price | |
| VWMA | Volume Weighted MA | |
| WAD | Williams Accumulation/Distribution | |
+35
View File
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Momentum</AssemblyName>
<AlgoType>Indicator</AlgoType>
<OutputPath>bin\$(Configuration)\</OutputPath>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<IsPackable>false</IsPackable>
<SonarQubeExclude>true</SonarQubeExclude>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<Compile Include="IndicatorExtensions.cs" />
<Compile Include="..\lib\core\**\*.cs" Exclude="..\lib\core\**\*.Tests.cs" />
<Compile Include="..\lib\momentum\**\*.cs" Exclude="..\lib\momentum\**\*.Tests.cs" />
<Compile Include="..\lib\trends\wma\Wma.cs" />
<Compile Include="..\lib\trends\pwma\Pwma.cs" />
<Reference Include="TradingPlatform.BusinessLayer">
<HintPath>..\.github\TradingPlatform.BusinessLayer.dll</HintPath>
</Reference>
<None Include="..\.github\TradingPlatform.BusinessLayer.xml">
<Link>TradingPlatform.BusinessLayer.xml</Link>
</None>
</ItemGroup>
<Target Name="CopyCustomContent" AfterTargets="AfterBuild" Condition="'$(IsLocalBuild)' == 'true' AND $([MSBuild]::IsOSPlatform('Windows'))">
<Copy SourceFiles="$(OutputPath)\Momentum.dll" DestinationFolder="$(QuantowerRoot)\Settings\Scripts\Indicators\QuanTAlib\Momentum" />
</Target>
</Project>
+2
View File
@@ -30,6 +30,8 @@
<Compile Include="..\lib\core\**\*.cs" Exclude="..\lib\core\**\*.Tests.cs" />
<!-- Include trends implementations -->
<Compile Include="..\lib\trends\**\*.cs" Exclude="..\lib\trends\**\*.Tests.cs;..\lib\trends\**\*.Validation.Tests.cs" />
<!-- Include momentum implementations -->
<Compile Include="..\lib\momentum\**\*.cs" Exclude="..\lib\momentum\**\*.Tests.cs;..\lib\momentum\**\*.Validation.Tests.cs" />
<!-- Include IndicatorExtensions -->
<Compile Include="IndicatorExtensions.cs" />
</ItemGroup>