mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-18 10:38:05 +00:00
Remove multiple Pine Script indicators: SSFDSP, STARCHANNEL, STBANDS, STC, UBANDS, UCHANNEL, VWAPBANDS, and VWAPSD. These indicators were deleted to streamline the library and remove unused or redundant code.
This commit is contained in:
@@ -12,6 +12,7 @@ Volume is market fuel. Price tells what happened; volume tells how hard the mark
|
||||
| [CMF](cmf/Cmf.md) | Chaikin Money Flow | Measures money flow volume over set period (typically 20-21 days). |
|
||||
| [EFI](efi/Efi.md) | Elder's Force Index | Combines price movement, direction, volume to measure buying/selling power. |
|
||||
| [EOM](eom/Eom.md) | Ease of Movement | Relates price change to volume. Highlights periods of effortless price movement. |
|
||||
| [EVWMA](evwma/Evwma.md) | Elastic Volume Weighted MA | Volume-adaptive moving average where volume directly controls smoothing weight per bar. |
|
||||
| [III](iii/Iii.md) | Intraday Intensity Index | Measures buying/selling pressure within day's range using close position. |
|
||||
| [KVO](kvo/Kvo.md) | Klinger Volume Oscillator | Compares short-term and long-term volume trends to identify potential reversals. |
|
||||
| [MFI](mfi/Mfi.md) | Money Flow Index | Volume-weighted RSI. Measures buying/selling pressure using price and volume. |
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EvwmaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void EvwmaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new EvwmaIndicator();
|
||||
|
||||
Assert.Equal("EVWMA - Elastic Volume Weighted Moving Average", indicator.Name);
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(20, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvwmaIndicator_ShortName_ReflectsPeriod()
|
||||
{
|
||||
var indicator = new EvwmaIndicator { Period = 14 };
|
||||
Assert.Equal("EVWMA(14)", indicator.ShortName);
|
||||
|
||||
var indicatorDefault = new EvwmaIndicator { Period = 20 };
|
||||
Assert.Equal("EVWMA(20)", indicatorDefault.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvwmaIndicator_MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var indicator = new EvwmaIndicator { Period = 10 };
|
||||
|
||||
Assert.Equal(10, indicator.MinHistoryDepths);
|
||||
Assert.Equal(10, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvwmaIndicator_Initialize_CreatesInternalEvwma()
|
||||
{
|
||||
var indicator = new EvwmaIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvwmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new EvwmaIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 1000);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvwmaIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new EvwmaIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 130, 140, 120, 135, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvwmaIndicator_Value_TracksVolumeWeightedAverage()
|
||||
{
|
||||
var indicator = new EvwmaIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var recordedValues = new List<double>();
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
// Create varying price patterns
|
||||
double open = 100 + i;
|
||||
double high = open + 10 + (i % 5);
|
||||
double low = open - 5;
|
||||
double close = (i % 2 == 0) ? high - 1 : low + 1;
|
||||
double vol = 1000 + (i * 100);
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), open, high, low, close, vol);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
recordedValues.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
// EVWMA should produce finite values
|
||||
Assert.True(recordedValues.Count > 0, "Should have recorded values");
|
||||
Assert.All(recordedValues, v => Assert.True(double.IsFinite(v)));
|
||||
|
||||
// EVWMA values should be within price range (approximately)
|
||||
double avgValue = recordedValues.Average();
|
||||
Assert.True(avgValue > 90 && avgValue < 200, $"EVWMA {avgValue} should be within reasonable price range");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvwmaIndicator_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var indicator5 = new EvwmaIndicator { Period = 5 };
|
||||
var indicator20 = new EvwmaIndicator { Period = 20 };
|
||||
|
||||
indicator5.Initialize();
|
||||
indicator20.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double open = 100 + i;
|
||||
double high = open + 10;
|
||||
double low = open - 5;
|
||||
double close = open + 5;
|
||||
double volume = 1000 + (i * 50);
|
||||
|
||||
indicator5.HistoricalData.AddBar(now.AddMinutes(i), open, high, low, close, volume);
|
||||
indicator20.HistoricalData.AddBar(now.AddMinutes(i), open, high, low, close, volume);
|
||||
|
||||
indicator5.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator20.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val5 = indicator5.LinesSeries[0].GetValue(0);
|
||||
double val20 = indicator20.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Different periods should produce different results
|
||||
Assert.NotEqual(val5, val20, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvwmaIndicator_SlidingWindow_DropsOldValues()
|
||||
{
|
||||
var indicator = new EvwmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add initial bars with constant price/volume
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 101, 99, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double valueAtConstant = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Add bars with higher prices - behavior should shift
|
||||
for (int i = 3; i < 6; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 200, 201, 199, 200, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double valueAfterHigh = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Value should have changed significantly as old volumes drop and new prices dominate
|
||||
Assert.True(valueAfterHigh > valueAtConstant + 50,
|
||||
$"EVWMA should increase as low-price bars drop out: {valueAtConstant} -> {valueAfterHigh}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Quantower adapter for EVWMA (Elastic Volume Weighted Moving Average).
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public sealed class EvwmaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 10, 1, 10000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Evwma _evwma = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"EVWMA({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volume/evwma/Evwma.Quantower.cs";
|
||||
|
||||
public EvwmaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "EVWMA - Elastic Volume Weighted Moving Average";
|
||||
Description = "Elastic Volume Weighted Moving Average weights each bar by its volume relative to a rolling volume sum over a specified period";
|
||||
|
||||
_series = new LineSeries(name: "EVWMA", color: Color.Yellow, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_evwma = new Evwma(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _evwma.Update(bar, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _evwma.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EvwmaTests
|
||||
{
|
||||
private readonly GBM _feed;
|
||||
private readonly TBarSeries _bars;
|
||||
|
||||
public EvwmaTests()
|
||||
{
|
||||
_feed = new GBM();
|
||||
_bars = new TBarSeries();
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
_bars.Add(_feed.Next());
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Constructor Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultPeriod_ShouldBe20()
|
||||
{
|
||||
var evwma = new Evwma();
|
||||
Assert.Equal("EVWMA(20)", evwma.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithPeriod_ShouldSetName()
|
||||
{
|
||||
var evwma = new Evwma(14);
|
||||
Assert.Equal("EVWMA(14)", evwma.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroPeriod_ShouldThrow()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Evwma(0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_ShouldThrow()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Evwma(-1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
// ============ Basic Calculation Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsValidTValue()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
var bar = _bars[0];
|
||||
var result = evwma.Update(bar);
|
||||
|
||||
Assert.NotEqual(default, result);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_ShouldBeClosePrice()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
var bar = new TBar(DateTime.UtcNow, 10, 15, 8, 12, 1000);
|
||||
var result = evwma.Update(bar);
|
||||
|
||||
// EVWMA of first bar = close price (only one data point)
|
||||
Assert.Equal(12.0, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleBarsSamePrice_ShouldReturnSameEvwma()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
// All bars have same close price = 100
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 100);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 100, 100, 100, 100, 200);
|
||||
var bar3 = new TBar(DateTime.UtcNow.AddMinutes(2), 100, 100, 100, 100, 300);
|
||||
|
||||
evwma.Update(bar1);
|
||||
evwma.Update(bar2);
|
||||
var result = evwma.Update(bar3);
|
||||
|
||||
Assert.Equal(100.0, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_VolumeWeighting_Works()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
// Bar 1: price=10, volume=100 → result = 10 (first bar)
|
||||
// Bar 2: price=20, volume=300
|
||||
// sumVol = 100 + 300 = 400, remainVol = 400 - 300 = 100
|
||||
// result = (100 * 10 + 300 * 20) / 400 = (1000 + 6000) / 400 = 17.5
|
||||
var bar1 = new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 20, 20, 20, 20, 300);
|
||||
|
||||
evwma.Update(bar1);
|
||||
var result = evwma.Update(bar2);
|
||||
|
||||
Assert.Equal(17.5, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SlidingWindow_ShouldDropOldVolume()
|
||||
{
|
||||
// Period=2: rolling volume window holds 2 bars
|
||||
var evwma = new Evwma(2);
|
||||
|
||||
// Bar 1: price=100, vol=1000
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000);
|
||||
evwma.Update(bar1);
|
||||
Assert.Equal(100.0, evwma.Last.Value, 10); // First bar = price
|
||||
|
||||
// Bar 2: price=200, vol=1000
|
||||
// sumVol = 1000 + 1000 = 2000, remainVol = 2000 - 1000 = 1000
|
||||
// result = (1000 * 100 + 1000 * 200) / 2000 = 150
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 1000);
|
||||
evwma.Update(bar2);
|
||||
Assert.Equal(150.0, evwma.Last.Value, 10);
|
||||
|
||||
// Bar 3: price=300, vol=1000
|
||||
// Old bar1 vol drops out: sumVol = 1000(bar2) + 1000(bar3) = 2000
|
||||
// remainVol = 2000 - 1000 = 1000
|
||||
// result = (1000 * 150 + 1000 * 300) / 2000 = 225
|
||||
var bar3 = new TBar(DateTime.UtcNow.AddMinutes(2), 300, 300, 300, 300, 1000);
|
||||
var result = evwma.Update(bar3);
|
||||
|
||||
Assert.Equal(225.0, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterPeriodBars_ShouldBeTrue()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
Assert.False(evwma.IsHot);
|
||||
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
evwma.Update(_bars[i]);
|
||||
Assert.False(evwma.IsHot);
|
||||
}
|
||||
|
||||
evwma.Update(_bars[9]);
|
||||
Assert.True(evwma.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_ShouldMatchPeriod()
|
||||
{
|
||||
var evwma = new Evwma(14);
|
||||
Assert.Equal(14, evwma.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ============ Bar Correction Tests (isNew) ============
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_ShouldAdvanceState()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
var bar1 = new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 20, 20, 20, 20, 100);
|
||||
|
||||
evwma.Update(bar1, isNew: true);
|
||||
var result1 = evwma.Last.Value;
|
||||
|
||||
evwma.Update(bar2, isNew: true);
|
||||
var result2 = evwma.Last.Value;
|
||||
|
||||
Assert.NotEqual(result1, result2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_ShouldRollback()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
var bar1 = new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 20, 20, 20, 20, 100);
|
||||
var bar2Updated = new TBar(DateTime.UtcNow.AddMinutes(1), 15, 15, 15, 15, 100);
|
||||
|
||||
evwma.Update(bar1, isNew: true);
|
||||
evwma.Update(bar2, isNew: true);
|
||||
var afterBar2 = evwma.Last.Value;
|
||||
|
||||
// Correct bar2 with updated values
|
||||
evwma.Update(bar2Updated, isNew: false);
|
||||
var afterCorrection = evwma.Last.Value;
|
||||
|
||||
Assert.NotEqual(afterBar2, afterCorrection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_ShouldRestoreState()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
|
||||
// Process first 10 bars
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
evwma.Update(_bars[i], isNew: true);
|
||||
}
|
||||
_ = evwma.Last.Value;
|
||||
|
||||
// Process bar 11
|
||||
evwma.Update(_bars[10], isNew: true);
|
||||
var valueAfter11 = evwma.Last.Value;
|
||||
|
||||
// Correct bar 11 multiple times with same data
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
evwma.Update(_bars[10], isNew: false);
|
||||
}
|
||||
var valueAfterCorrections = evwma.Last.Value;
|
||||
|
||||
// Should get same result as after first processing of bar 11
|
||||
Assert.Equal(valueAfter11, valueAfterCorrections, 10);
|
||||
}
|
||||
|
||||
// ============ Reset Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Reset_ShouldClearState()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
evwma.Update(_bars[i]);
|
||||
}
|
||||
Assert.True(evwma.IsHot);
|
||||
|
||||
evwma.Reset();
|
||||
|
||||
Assert.False(evwma.IsHot);
|
||||
Assert.Equal(default, evwma.Last);
|
||||
}
|
||||
|
||||
// ============ NaN/Infinity Handling ============
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_ShouldUseLastValidValue()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
|
||||
// First bar establishes valid values
|
||||
var bar1 = new TBar(DateTime.UtcNow, 10, 15, 8, 12, 1000);
|
||||
evwma.Update(bar1);
|
||||
|
||||
// Second bar with NaN should use last valid
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), double.NaN, double.NaN, double.NaN, double.NaN, double.NaN);
|
||||
var result = evwma.Update(bar2);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_ShouldUseLastValidValue()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 10, 15, 8, 12, 1000);
|
||||
evwma.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity);
|
||||
var result = evwma.Update(bar2);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
// ============ TValue Input Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_ShouldWork()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
var result = evwma.Update(input);
|
||||
|
||||
// With TValue, it uses value as price and volume=1
|
||||
Assert.Equal(100.0, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_MultipleInputs()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
|
||||
// TValue input assumes volume=1 for all
|
||||
// Bar 1: price=100, vol=1 → result = 100
|
||||
// Bar 2: price=200, vol=1
|
||||
// sumVol = 1 + 1 = 2, remainVol = 2 - 1 = 1
|
||||
// result = (1 * 100 + 1 * 200) / 2 = 150
|
||||
evwma.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
var result = evwma.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 200.0));
|
||||
|
||||
Assert.Equal(150.0, result.Value, 10);
|
||||
}
|
||||
|
||||
// ============ Batch/Series Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Update_TBarSeries_ShouldReturnTSeries()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
var result = evwma.Update(_bars);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(_bars.Count, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_ShouldReturnTSeries()
|
||||
{
|
||||
var result = Evwma.Batch(_bars, 10);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(_bars.Count, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_WithDifferentPeriods_ShouldWork()
|
||||
{
|
||||
var result14 = Evwma.Batch(_bars, 14);
|
||||
var result50 = Evwma.Batch(_bars, 50);
|
||||
|
||||
Assert.NotNull(result14);
|
||||
Assert.NotNull(result50);
|
||||
Assert.Equal(_bars.Count, result14.Count);
|
||||
Assert.Equal(_bars.Count, result50.Count);
|
||||
}
|
||||
|
||||
// ============ Span API Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_ShouldMatchBatch()
|
||||
{
|
||||
var batchResult = Evwma.Batch(_bars, 20);
|
||||
|
||||
var price = _bars.Close.Values.ToArray();
|
||||
var volume = _bars.Volume.Values.ToArray();
|
||||
var spanOutput = new double[_bars.Count];
|
||||
|
||||
Evwma.Batch(price, volume, spanOutput, 20);
|
||||
|
||||
for (int i = 0; i < _bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], spanOutput[i], 12);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_MismatchedLengths_ShouldThrow()
|
||||
{
|
||||
var price = new double[100];
|
||||
var volume = new double[99]; // Mismatched
|
||||
var output = new double[100];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Evwma.Batch(price, volume, output, 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_OutputLengthMismatch_ShouldThrow()
|
||||
{
|
||||
var price = new double[100];
|
||||
var volume = new double[100];
|
||||
var output = new double[50]; // Mismatched
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Evwma.Batch(price, volume, output, 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_ZeroPeriod_ShouldThrow()
|
||||
{
|
||||
var price = new double[100];
|
||||
var volume = new double[100];
|
||||
var output = new double[100];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Evwma.Batch(price, volume, output, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_NegativePeriod_ShouldThrow()
|
||||
{
|
||||
var price = new double[100];
|
||||
var volume = new double[100];
|
||||
var output = new double[100];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Evwma.Batch(price, volume, output, -1));
|
||||
}
|
||||
|
||||
// ============ Event Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Pub_ShouldFireOnUpdate()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
int eventCount = 0;
|
||||
|
||||
evwma.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
evwma.Update(_bars[0]);
|
||||
evwma.Update(_bars[1]);
|
||||
|
||||
Assert.Equal(2, eventCount);
|
||||
}
|
||||
|
||||
// ============ Streaming/Batch Consistency ============
|
||||
|
||||
[Fact]
|
||||
public void Streaming_ShouldMatchBatch()
|
||||
{
|
||||
// Streaming
|
||||
var evwma = new Evwma(20);
|
||||
var streamingResults = new List<double>();
|
||||
foreach (var bar in _bars)
|
||||
{
|
||||
streamingResults.Add(evwma.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Evwma.Batch(_bars, 20);
|
||||
|
||||
// Compare last 100 values
|
||||
for (int i = _bars.Count - 100; i < _bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], streamingResults[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ TSeries Calculate Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Calculate_TSeries_ShouldWork()
|
||||
{
|
||||
var sourceSeries = _bars.Close;
|
||||
var result = Evwma.Batch(sourceSeries, 20);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(sourceSeries.Count, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_TSeries_ShouldMatchTValueStreaming()
|
||||
{
|
||||
var sourceSeries = _bars.Close;
|
||||
var batchResult = Evwma.Batch(sourceSeries, 20);
|
||||
|
||||
// Streaming with TValue
|
||||
var evwma = new Evwma(20);
|
||||
var streamingResults = new List<double>();
|
||||
for (int i = 0; i < sourceSeries.Count; i++)
|
||||
{
|
||||
streamingResults.Add(evwma.Update(sourceSeries[i]).Value);
|
||||
}
|
||||
|
||||
// Compare last 100 values
|
||||
for (int i = sourceSeries.Count - 100; i < sourceSeries.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], streamingResults[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ EVWMA-Specific Volume Behavior Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Update_HighVolumeBar_ShouldShiftMoreThanLowVolume()
|
||||
{
|
||||
// Two EVWMA instances, same initial state
|
||||
var evwma1 = new Evwma(10);
|
||||
var evwma2 = new Evwma(10);
|
||||
|
||||
// Same warmup bars
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
evwma1.Update(bar);
|
||||
evwma2.Update(bar);
|
||||
}
|
||||
|
||||
// evwma1: new bar at price 200 with HIGH volume
|
||||
var highVolBar = new TBar(DateTime.UtcNow.AddMinutes(10), 200, 200, 200, 200, 10000);
|
||||
evwma1.Update(highVolBar);
|
||||
|
||||
// evwma2: same price but LOW volume
|
||||
var lowVolBar = new TBar(DateTime.UtcNow.AddMinutes(10), 200, 200, 200, 200, 10);
|
||||
evwma2.Update(lowVolBar);
|
||||
|
||||
// High volume bar should shift the average more toward 200
|
||||
Assert.True(evwma1.Last.Value > evwma2.Last.Value,
|
||||
$"High volume EVWMA ({evwma1.Last.Value}) should be closer to 200 than low volume EVWMA ({evwma2.Last.Value})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ZeroVolume_ShouldNotChangeResult()
|
||||
{
|
||||
var evwma = new Evwma(10);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000);
|
||||
evwma.Update(bar1);
|
||||
var afterFirst = evwma.Last.Value;
|
||||
|
||||
// Zero volume bar should not affect the average
|
||||
var zeroVolBar = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 0);
|
||||
evwma.Update(zeroVolBar);
|
||||
|
||||
// With zero volume, the denominator changes but curVol=0 means no new price impact
|
||||
// result = ((sumVol - 0) * prevResult + 0 * curPrice) / sumVol = prevResult
|
||||
Assert.Equal(afterFirst, evwma.Last.Value, 10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EvwmaValidationTests
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
|
||||
public EvwmaValidationTests()
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
}
|
||||
|
||||
// ============ External Library Validation ============
|
||||
// EVWMA is not available in standard libraries (Skender, TA-Lib, Tulip, Ooples).
|
||||
// Validation is performed via internal consistency and known-value tests.
|
||||
|
||||
[Fact]
|
||||
public void Evwma_NotAvailable_Skender()
|
||||
{
|
||||
// Skender.Stock.Indicators does not have EVWMA
|
||||
Assert.True(true, "EVWMA is not available in Skender");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evwma_NotAvailable_Talib()
|
||||
{
|
||||
// TA-Lib does not have EVWMA
|
||||
Assert.True(true, "EVWMA is not available in TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evwma_NotAvailable_Tulip()
|
||||
{
|
||||
// Tulip does not have EVWMA
|
||||
Assert.True(true, "EVWMA is not available in Tulip");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evwma_NotAvailable_Ooples()
|
||||
{
|
||||
// Ooples does not have EVWMA
|
||||
Assert.True(true, "EVWMA is not available in Ooples");
|
||||
}
|
||||
|
||||
// ============ Internal Consistency Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Evwma_Streaming_Matches_Batch()
|
||||
{
|
||||
int period = 20;
|
||||
|
||||
// Streaming
|
||||
var evwma = new Evwma(period);
|
||||
var streamingValues = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
streamingValues.Add(evwma.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Evwma.Batch(_data.Bars, period);
|
||||
var batchValues = batchResult.Values.ToArray();
|
||||
|
||||
ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evwma_Span_Matches_Streaming()
|
||||
{
|
||||
int period = 20;
|
||||
|
||||
// Streaming
|
||||
var evwma = new Evwma(period);
|
||||
var streamingValues = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
streamingValues.Add(evwma.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Span
|
||||
var price = _data.Bars.Close.Values.ToArray();
|
||||
var volume = _data.Bars.Volume.Values.ToArray();
|
||||
var spanValues = new double[price.Length];
|
||||
Evwma.Batch(price, volume, spanValues, period);
|
||||
|
||||
ValidationHelper.VerifyData(streamingValues.ToArray(), spanValues, 0, 100, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evwma_Batch_Matches_Span()
|
||||
{
|
||||
int period = 20;
|
||||
|
||||
// Batch
|
||||
var batchResult = Evwma.Batch(_data.Bars, period);
|
||||
var batchValues = batchResult.Values.ToArray();
|
||||
|
||||
// Span
|
||||
var price = _data.Bars.Close.Values.ToArray();
|
||||
var volume = _data.Bars.Volume.Values.ToArray();
|
||||
var spanValues = new double[price.Length];
|
||||
Evwma.Batch(price, volume, spanValues, period);
|
||||
|
||||
// Batch and Span use identical code path, should match exactly
|
||||
ValidationHelper.VerifyData(batchValues, spanValues, 0, 100, 1e-12);
|
||||
}
|
||||
|
||||
// ============ Known-Value Validation ============
|
||||
|
||||
[Fact]
|
||||
public void Evwma_KnownValues_ManualCalculation()
|
||||
{
|
||||
// Manually compute EVWMA for a small series
|
||||
// Period = 3
|
||||
// Bar 0: price=100, vol=10 → sumVol=10, result=100 (first bar)
|
||||
// Bar 1: price=110, vol=20 → sumVol=30, remain=10, result=(10*100+20*110)/30=3200/30≈106.6667
|
||||
// Bar 2: price=105, vol=15 → sumVol=45, remain=30, result=(30*106.6667+15*105)/45=4725/45=105
|
||||
// Wait: (30 × 106.6667 + 15 × 105) / 45 = (3200 + 1575) / 45 = 4775/45 ≈ 106.1111
|
||||
// Bar 3: price=120, vol=25 → drop bar0 vol(10): sumVol=45-10+25=60, remain=35
|
||||
// result = (35 * 106.1111 + 25 * 120) / 60 = (3713.889 + 3000) / 60 = 6713.889/60 ≈ 111.898
|
||||
|
||||
var evwma = new Evwma(3);
|
||||
|
||||
var bar0 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 10);
|
||||
var r0 = evwma.Update(bar0);
|
||||
Assert.Equal(100.0, r0.Value, 6);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow.AddMinutes(1), 110, 110, 110, 110, 20);
|
||||
var r1 = evwma.Update(bar1);
|
||||
// sumVol = 10 + 20 = 30; remainVol = 30 - 20 = 10
|
||||
// result = (10 * 100 + 20 * 110) / 30 = 3200 / 30
|
||||
Assert.Equal(3200.0 / 30.0, r1.Value, 10);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(2), 105, 105, 105, 105, 15);
|
||||
var r2 = evwma.Update(bar2);
|
||||
// sumVol = 10 + 20 + 15 = 45; remainVol = 45 - 15 = 30
|
||||
// prevResult = 3200/30
|
||||
// result = (30 * (3200/30) + 15 * 105) / 45 = (3200 + 1575) / 45 = 4775 / 45
|
||||
Assert.Equal(4775.0 / 45.0, r2.Value, 10);
|
||||
|
||||
var bar3 = new TBar(DateTime.UtcNow.AddMinutes(3), 120, 120, 120, 120, 25);
|
||||
var r3 = evwma.Update(bar3);
|
||||
// Bar0 vol drops: sumVol = (10+20+15) - 10 + 25 = 60; remainVol = 60 - 25 = 35
|
||||
// prevResult = 4775/45
|
||||
// result = (35 * (4775/45) + 25 * 120) / 60
|
||||
double prev = 4775.0 / 45.0;
|
||||
double expected = (35.0 * prev + 25.0 * 120.0) / 60.0;
|
||||
Assert.Equal(expected, r3.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evwma_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
int period1 = 5;
|
||||
int period2 = 50;
|
||||
|
||||
var result1 = Evwma.Batch(_data.Bars, period1);
|
||||
var result2 = Evwma.Batch(_data.Bars, period2);
|
||||
|
||||
// At later bars, different periods should produce different values
|
||||
int idx = 100;
|
||||
Assert.NotEqual(result1.Values[idx], result2.Values[idx], 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evwma_ConstantPrice_ReturnsConstant()
|
||||
{
|
||||
// If all prices are the same, EVWMA should always return that price
|
||||
// regardless of volume
|
||||
int period = 10;
|
||||
var bars = new TBarSeries();
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
bars.Add(new TBar(now.AddMinutes(i), 42.0, 42.0, 42.0, 42.0, 100 + i * 10));
|
||||
}
|
||||
|
||||
var result = Evwma.Batch(bars, period);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
Assert.Equal(42.0, result.Values[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evwma_UniformVolume_BehavesLikeRunningAverage()
|
||||
{
|
||||
// With uniform volume=1 and period covering all bars,
|
||||
// EVWMA degenerates to a specific recursive average
|
||||
int period = 100;
|
||||
var evwma = new Evwma(period);
|
||||
|
||||
double[] prices = [100, 110, 105, 120, 95, 115, 108, 112, 103, 118];
|
||||
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
var tv = new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]);
|
||||
evwma.Update(tv);
|
||||
}
|
||||
|
||||
// Result should be finite and within the price range
|
||||
Assert.True(double.IsFinite(evwma.Last.Value));
|
||||
Assert.True(evwma.Last.Value >= 90 && evwma.Last.Value <= 130,
|
||||
$"EVWMA value {evwma.Last.Value} should be within price range");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the Elastic Volume Weighted Moving Average (EVWMA) over a fixed lookback period.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// EVWMA weights each bar elastically by its volume relative to the rolling volume sum:
|
||||
/// <c>EVWMA = ((sumVol - curVol) * prevResult + curVol * curPrice) / sumVol</c>.
|
||||
///
|
||||
/// High-volume bars shift the average more aggressively; low-volume bars barely nudge it.
|
||||
/// The rolling volume sum uses a circular buffer for O(1) streaming updates.
|
||||
///
|
||||
/// Non-finite inputs (NaN/±Inf) are sanitized by substituting the last finite value observed
|
||||
/// for price and volume independently.
|
||||
///
|
||||
/// For the authoritative algorithm reference, full rationale, and behavioral contracts, see the
|
||||
/// companion files in the same directory.
|
||||
/// </remarks>
|
||||
/// <seealso href="Evwma.md">Detailed documentation</seealso>
|
||||
/// <seealso href="evwma.pine">Reference Pine Script implementation</seealso>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Evwma : ITValuePublisher
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double SumVol, double Result, int Index, int Head, int Count, int SyncCounter)
|
||||
{
|
||||
public static State New() => new() { SumVol = 0, Result = double.NaN, Index = 0, Head = 0, Count = 0, SyncCounter = 0 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resync interval to limit floating-point drift in running volume sum.
|
||||
/// Full recalculation every N bars.
|
||||
/// </summary>
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
private readonly int _period;
|
||||
private readonly double[] _volBuffer;
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
private double _lastValidClose;
|
||||
private double _lastValidVolume;
|
||||
private double _p_lastValidClose;
|
||||
private double _p_lastValidVolume;
|
||||
private double _p_bufferVol; // Previous volume at current head position
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Current EVWMA value.
|
||||
/// </summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has processed at least Period bars.
|
||||
/// </summary>
|
||||
public bool IsHot => _state.Count >= _period;
|
||||
|
||||
/// <summary>
|
||||
/// Warmup period equals the specified period.
|
||||
/// </summary>
|
||||
// S2325 suppressed: Instance property required for interface consistency across all indicators,
|
||||
// even when value is constant. All QuanTAlib indicators expose WarmupPeriod as instance property.
|
||||
#pragma warning disable S2325
|
||||
public int WarmupPeriod => _period;
|
||||
#pragma warning restore S2325
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new EVWMA indicator.
|
||||
/// </summary>
|
||||
/// <param name="period">Lookback period for rolling volume sum. Must be >= 1.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when period is less than 1.</exception>
|
||||
public Evwma(int period = 20)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be >= 1", nameof(period));
|
||||
}
|
||||
|
||||
_period = period;
|
||||
_volBuffer = new double[period];
|
||||
_state = State.New();
|
||||
_p_state = State.New();
|
||||
Name = $"EVWMA({period})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the indicator state.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_state = State.New();
|
||||
_p_state = State.New();
|
||||
Array.Clear(_volBuffer);
|
||||
_lastValidClose = 0;
|
||||
_lastValidVolume = 0;
|
||||
_p_lastValidClose = 0;
|
||||
_p_lastValidVolume = 0;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double GetValidValue(double input, ref double lastValid)
|
||||
{
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
lastValid = input;
|
||||
return input;
|
||||
}
|
||||
return lastValid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recalculates running volume sum from buffer to eliminate accumulated floating-point drift.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ResyncRunningTotals(ref State s)
|
||||
{
|
||||
double sumVol = 0;
|
||||
|
||||
for (int i = 0; i < _period; i++)
|
||||
{
|
||||
double v = _volBuffer[i];
|
||||
if (v > 0)
|
||||
{
|
||||
sumVol += v;
|
||||
}
|
||||
}
|
||||
|
||||
s.SumVol = sumVol;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
return UpdateInternal(input.Time, input.Close, input.Volume, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates EVWMA with a TValue input (uses value as price, assumes volume=1).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
return UpdateInternal(input.Time, input.Value, 1.0, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EVWMA for an entire bar series.
|
||||
/// </summary>
|
||||
/// <param name="source">Source bar series</param>
|
||||
/// <returns>TSeries containing EVWMA values</returns>
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var t = new List<long>(source.Count);
|
||||
var v = new List<double>(source.Count);
|
||||
|
||||
Reset();
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var val = Update(source[i], isNew: true);
|
||||
t.Add(val.Time);
|
||||
v.Add(val.Value);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private TValue UpdateInternal(long time, double price, double volume, bool isNew)
|
||||
{
|
||||
// Local copy for struct promotion
|
||||
var s = _state;
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
_p_lastValidClose = _lastValidClose;
|
||||
_p_lastValidVolume = _lastValidVolume;
|
||||
// Save current buffer value at head position for rollback
|
||||
_p_bufferVol = _volBuffer[s.Head];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore previous state
|
||||
s = _p_state;
|
||||
_state = _p_state;
|
||||
_lastValidClose = _p_lastValidClose;
|
||||
_lastValidVolume = _p_lastValidVolume;
|
||||
// Restore buffer value at head position
|
||||
_volBuffer[s.Head] = _p_bufferVol;
|
||||
}
|
||||
|
||||
// Get valid values
|
||||
double currentPrice = GetValidValue(price, ref _lastValidClose);
|
||||
double currentVol = GetValidValue(volume, ref _lastValidVolume);
|
||||
currentVol = Math.Max(0.0, currentVol);
|
||||
|
||||
// Remove oldest volume from circular buffer
|
||||
double oldVol = _volBuffer[s.Head];
|
||||
|
||||
if (s.Count >= _period)
|
||||
{
|
||||
s.SumVol -= oldVol;
|
||||
}
|
||||
|
||||
// Add current volume to running sum
|
||||
s.SumVol += currentVol;
|
||||
|
||||
// Store in circular buffer
|
||||
_volBuffer[s.Head] = currentVol;
|
||||
|
||||
// Advance head pointer
|
||||
s.Head = (s.Head + 1) % _period;
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
s.Index++;
|
||||
if (s.Count < _period)
|
||||
{
|
||||
s.Count++;
|
||||
}
|
||||
|
||||
// Periodic resync to limit floating-point drift
|
||||
s.SyncCounter++;
|
||||
if (s.SyncCounter >= ResyncInterval && s.Count >= _period)
|
||||
{
|
||||
s.SyncCounter = 0;
|
||||
ResyncRunningTotals(ref s);
|
||||
}
|
||||
}
|
||||
|
||||
// EVWMA calculation
|
||||
double result;
|
||||
if (double.IsNaN(s.Result))
|
||||
{
|
||||
// First bar: initialize to current price
|
||||
result = currentPrice;
|
||||
}
|
||||
else if (s.SumVol > double.Epsilon)
|
||||
{
|
||||
// EVWMA = ((sumVol - curVol) * prevResult + curVol * curPrice) / sumVol
|
||||
double remainVol = s.SumVol - currentVol;
|
||||
result = Math.FusedMultiplyAdd(remainVol, s.Result, currentVol * currentPrice) / s.SumVol;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = s.Result;
|
||||
}
|
||||
|
||||
s.Result = result;
|
||||
_state = s;
|
||||
|
||||
Last = new TValue(time, result);
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical bar data.</param>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static calculation returning TSeries from bar series.
|
||||
/// </summary>
|
||||
/// <param name="source">Source bar series</param>
|
||||
/// <param name="period">Lookback period for rolling volume sum</param>
|
||||
/// <returns>TSeries containing EVWMA values</returns>
|
||||
public static TSeries Batch(TBarSeries source, int period = 20)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var t = source.Open.Times.ToArray();
|
||||
var v = new double[source.Count];
|
||||
|
||||
Batch(source.Close.Values, source.Volume.Values, v, period);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static calculation for TSeries (price with assumed volume=1).
|
||||
/// </summary>
|
||||
/// <param name="source">Source value series</param>
|
||||
/// <param name="period">Lookback period for rolling volume sum</param>
|
||||
/// <returns>TSeries containing EVWMA values</returns>
|
||||
public static TSeries Batch(TSeries source, int period = 20)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var t = source.Times.ToArray();
|
||||
var v = new double[source.Count];
|
||||
|
||||
// Use span overload with uniform volume = 1
|
||||
Span<double> unitVolume = stackalloc double[source.Count];
|
||||
unitVolume.Fill(1.0);
|
||||
|
||||
Batch(source.Values, unitVolume, v, period);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zero-allocation span-based calculation.
|
||||
/// </summary>
|
||||
/// <param name="source">Source price values</param>
|
||||
/// <param name="volume">Volume values</param>
|
||||
/// <param name="output">Output span for EVWMA values</param>
|
||||
/// <param name="period">Lookback period for rolling volume sum</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public static void Batch(ReadOnlySpan<double> source, ReadOnlySpan<double> volume, Span<double> output, int period = 20)
|
||||
{
|
||||
if (source.Length != volume.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and Volume spans must be of the same length", nameof(volume));
|
||||
}
|
||||
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Output span must be of the same length as input", nameof(output));
|
||||
}
|
||||
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be >= 1", nameof(period));
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int StackallocThreshold = 256;
|
||||
double[]? rentedVol = null;
|
||||
scoped Span<double> volBuffer;
|
||||
|
||||
if (period <= StackallocThreshold)
|
||||
{
|
||||
volBuffer = stackalloc double[period];
|
||||
}
|
||||
else
|
||||
{
|
||||
rentedVol = System.Buffers.ArrayPool<double>.Shared.Rent(period);
|
||||
volBuffer = rentedVol.AsSpan(0, period);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
volBuffer.Clear();
|
||||
|
||||
double sumVol = 0;
|
||||
double result = double.NaN;
|
||||
double lastValidPrice = 0;
|
||||
double lastValidVolume = 0;
|
||||
int head = 0;
|
||||
int count = 0;
|
||||
|
||||
// Find first valid values
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
if (double.IsFinite(source[k]))
|
||||
{
|
||||
lastValidPrice = source[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
if (double.IsFinite(volume[k]))
|
||||
{
|
||||
lastValidVolume = volume[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int syncCounter = 0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
// Get valid values with NaN substitution
|
||||
double currentPrice = double.IsFinite(source[i]) ? source[i] : lastValidPrice;
|
||||
double currentVol = double.IsFinite(volume[i]) ? volume[i] : lastValidVolume;
|
||||
currentVol = Math.Max(0.0, currentVol);
|
||||
|
||||
if (double.IsFinite(source[i]))
|
||||
{
|
||||
lastValidPrice = source[i];
|
||||
}
|
||||
if (double.IsFinite(volume[i]))
|
||||
{
|
||||
lastValidVolume = volume[i];
|
||||
}
|
||||
|
||||
// Remove oldest volume from circular buffer
|
||||
double oldVol = volBuffer[head];
|
||||
|
||||
if (count >= period)
|
||||
{
|
||||
sumVol -= oldVol;
|
||||
}
|
||||
|
||||
// Add current volume to running sum
|
||||
sumVol += currentVol;
|
||||
|
||||
// Store in circular buffer
|
||||
volBuffer[head] = currentVol;
|
||||
|
||||
// Advance head pointer
|
||||
head = (head + 1) % period;
|
||||
|
||||
if (count < period)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
// Periodic resync to limit floating-point drift
|
||||
syncCounter++;
|
||||
if (syncCounter >= ResyncInterval && count >= period)
|
||||
{
|
||||
syncCounter = 0;
|
||||
sumVol = 0;
|
||||
for (int j = 0; j < period; j++)
|
||||
{
|
||||
double vj = volBuffer[j];
|
||||
if (vj > 0)
|
||||
{
|
||||
sumVol += vj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EVWMA calculation
|
||||
if (double.IsNaN(result))
|
||||
{
|
||||
result = currentPrice;
|
||||
}
|
||||
else if (sumVol > double.Epsilon)
|
||||
{
|
||||
double remainVol = sumVol - currentVol;
|
||||
result = Math.FusedMultiplyAdd(remainVol, result, currentVol * currentPrice) / sumVol;
|
||||
}
|
||||
|
||||
output[i] = result;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedVol != null)
|
||||
{
|
||||
System.Buffers.ArrayPool<double>.Shared.Return(rentedVol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Results, Evwma Indicator) Calculate(TBarSeries source, int period = 20)
|
||||
{
|
||||
var indicator = new Evwma(period);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
# EVWMA: Elastic Volume Weighted Moving Average
|
||||
|
||||
> "Volume is the one technical indicator that never lies." — Joe Granville
|
||||
|
||||
## Introduction
|
||||
|
||||
EVWMA (Elastic Volume Weighted Moving Average) is a volume-adaptive moving average that weights each bar's contribution to the average by its volume relative to a rolling volume sum. High-volume bars shift the average more aggressively toward the current price; low-volume bars barely nudge it. The "elastic" behavior emerges from volume-proportional blending: the smoothing factor is not fixed (like EMA's alpha) but varies dynamically with each bar's volume share.
|
||||
|
||||
Unlike VWMA (which computes a windowed weighted mean), EVWMA is recursive. Each output depends on the previous output, making it structurally closer to an EMA with a variable smoothing factor than to a simple weighted average.
|
||||
|
||||
## Historical Context
|
||||
|
||||
EVWMA was introduced by Christian P. Fries as a volume-aware alternative to traditional exponential smoothing. The core insight: fixed-alpha smoothing treats all bars identically regardless of trading activity. A bar with 10x normal volume carries the same weight as a quiet bar. EVWMA corrects this by making the effective smoothing factor proportional to the current bar's volume share within the lookback window.
|
||||
|
||||
Several implementations exist in the wild, most derived from the same recursive formula. The rolling volume sum approach (circular buffer) was popularized by TradingView implementations and provides O(1) streaming updates without requiring a full window rescan.
|
||||
|
||||
## Architecture and Physics
|
||||
|
||||
### 1. Rolling Volume Sum (Circular Buffer)
|
||||
|
||||
A circular buffer of length `period` tracks historical volumes. On each bar:
|
||||
|
||||
1. Remove the oldest volume from the running sum
|
||||
2. Add the current volume to the running sum
|
||||
3. Overwrite the oldest slot with current volume
|
||||
|
||||
This yields O(1) per-bar cost for maintaining the volume denominator.
|
||||
|
||||
### 2. Recursive EVWMA Calculation
|
||||
|
||||
Given:
|
||||
|
||||
- `sumVol` = rolling sum of volumes over the last `period` bars
|
||||
- `curVol` = current bar's volume
|
||||
- `curPrice` = current bar's close price
|
||||
- `prevResult` = previous EVWMA value
|
||||
|
||||
The update rule:
|
||||
|
||||
$$
|
||||
\text{EVWMA}_t = \frac{(\text{sumVol} - \text{curVol}) \cdot \text{EVWMA}_{t-1} + \text{curVol} \cdot \text{curPrice}}{\text{sumVol}}
|
||||
$$
|
||||
|
||||
Equivalently, defining the effective alpha as $\alpha_t = \frac{\text{curVol}}{\text{sumVol}}$:
|
||||
|
||||
$$
|
||||
\text{EVWMA}_t = (1 - \alpha_t) \cdot \text{EVWMA}_{t-1} + \alpha_t \cdot \text{curPrice}
|
||||
$$
|
||||
|
||||
This is an EMA with a time-varying smoothing factor driven by volume proportion.
|
||||
|
||||
### 3. Initialization
|
||||
|
||||
First bar: `result = curPrice` (no prior state to blend with).
|
||||
|
||||
### 4. Edge Cases
|
||||
|
||||
- **Zero volume**: curVol = 0 means alpha = 0, so result = prevResult (no change). The price is ignored when volume is zero.
|
||||
- **All-zero volume window**: sumVol = 0, result holds at previous value.
|
||||
- **Volume clamping**: Negative volumes are clamped to zero.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Transfer Function
|
||||
|
||||
In the z-domain, EVWMA can be written as:
|
||||
|
||||
$$
|
||||
H(z) = \frac{\alpha_t}{1 - (1 - \alpha_t) z^{-1}}
|
||||
$$
|
||||
|
||||
where $\alpha_t = V_t / \sum_{k=0}^{P-1} V_{t-k}$ is the volume-dependent smoothing factor.
|
||||
|
||||
### Effective Alpha Range
|
||||
|
||||
- **Minimum alpha**: When curVol is tiny relative to sumVol (quiet bar amid heavy trading). Result barely moves.
|
||||
- **Maximum alpha**: When curVol dominates sumVol (volume spike). Result snaps toward current price.
|
||||
- **Uniform volume**: alpha = 1/count (degenerates toward a specific recursive average).
|
||||
|
||||
### FMA Optimization
|
||||
|
||||
The numerator uses fused multiply-add for reduced rounding error:
|
||||
|
||||
```text
|
||||
numerator = FMA(remainVol, prevResult, curVol * curPrice)
|
||||
```
|
||||
|
||||
where `remainVol = sumVol - curVol`.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
| Operation | Count per bar |
|
||||
|-----------|--------------|
|
||||
| Additions | 3 |
|
||||
| Subtractions | 2 |
|
||||
| Multiplications | 2 |
|
||||
| Divisions | 1 |
|
||||
| FMA | 1 |
|
||||
| Comparisons | 3-4 |
|
||||
| **Total** | **~12 FLOPs** |
|
||||
|
||||
### Memory
|
||||
|
||||
| Component | Size |
|
||||
|-----------|------|
|
||||
| Volume buffer | `period * 8` bytes |
|
||||
| State struct | ~48 bytes |
|
||||
| **Total** | `period * 8 + 48` bytes |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score (1-10) |
|
||||
|--------|-------------|
|
||||
| Responsiveness | 8 (volume-adaptive) |
|
||||
| Smoothness | 7 (recursive, single-pole) |
|
||||
| Lag | 7 (reduced on high-volume bars) |
|
||||
| Noise rejection | 6 (volume-gated) |
|
||||
|
||||
## Validation
|
||||
|
||||
### External Libraries
|
||||
|
||||
| Library | Available | Notes |
|
||||
|---------|-----------|-------|
|
||||
| Skender | No | Not implemented |
|
||||
| TA-Lib | No | Not implemented |
|
||||
| Tulip | No | Not implemented |
|
||||
| Ooples | No | Not implemented |
|
||||
|
||||
### Internal Consistency
|
||||
|
||||
All four API modes (streaming, batch TBarSeries, batch TSeries, span) produce identical results within floating-point tolerance (1e-10).
|
||||
|
||||
Known-value tests verify manual calculations against the recursive formula.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Confusing EVWMA with VWMA**: VWMA is a windowed weighted mean (non-recursive); EVWMA is recursive with volume-varying alpha. They converge when volume is uniform but diverge significantly with volume spikes.
|
||||
|
||||
2. **Zero-volume bars**: By design, zero-volume bars do not move the average. If your data source produces zero-volume bars (e.g., overnight gaps), EVWMA will "freeze" during those periods.
|
||||
|
||||
3. **Period selection**: The period controls the volume window, not a price window. A period of 20 means "rolling sum of last 20 bars' volume." Shorter periods make the volume-weighting more responsive but amplify noise from individual volume spikes.
|
||||
|
||||
4. **Negative volume data**: Some data feeds report negative volume for corrections or adjustments. EVWMA clamps volume to zero, treating negative volume as no-activity.
|
||||
|
||||
5. **Floating-point drift**: Running sums accumulate drift over thousands of bars. The implementation resyncs every 1000 bars by recalculating the volume sum from the buffer.
|
||||
|
||||
6. **First-bar sensitivity**: The first bar initializes to the current price regardless of volume. This creates a brief transient; wait for the full warmup period before trusting the output.
|
||||
|
||||
## References
|
||||
|
||||
- Fries, C. P. "Elastic Volume Weighted Moving Average." Technical analysis research notes.
|
||||
- Granville, J. "New Key to Stock Market Profits." Prentice-Hall, 1963. (Volume analysis foundations)
|
||||
- Ehlers, J. F. "Cybernetic Analysis for Stocks and Futures." Wiley, 2004. (Adaptive smoothing concepts)
|
||||
Reference in New Issue
Block a user