mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 13:58:04 +00:00
Add PWMA implementation and tests; enhance documentation
This commit is contained in:
@@ -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 | |
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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/)
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user