mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 13:38:05 +00:00
Add TRAMA implementation and comprehensive tests
- Implemented the TRAMA (Trend Regularity Adaptive Moving Average) class with adaptive EMA logic. - Added unit tests for TRAMA functionality, including constructor validation, basic calculations, state management, and robustness checks. - Created validation tests to ensure consistency across different modes of operation (streaming, batch, and static calculations). - Enhanced documentation for TRAMA, including performance profiles and quality metrics. - Updated workspace configuration by removing unnecessary folder references.
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RwmaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void RwmaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new RwmaIndicator();
|
||||
|
||||
Assert.Equal("RWMA - Range Weighted Moving Average", indicator.Name);
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(14, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RwmaIndicator_ShortName_ReflectsPeriod()
|
||||
{
|
||||
var indicator = new RwmaIndicator { Period = 10 };
|
||||
Assert.Equal("RWMA(10)", indicator.ShortName);
|
||||
|
||||
var indicatorDefault = new RwmaIndicator { Period = 14 };
|
||||
Assert.Equal("RWMA(14)", indicatorDefault.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RwmaIndicator_MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var indicator = new RwmaIndicator { Period = 10 };
|
||||
|
||||
Assert.Equal(10, indicator.MinHistoryDepths);
|
||||
Assert.Equal(10, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RwmaIndicator_Initialize_CreatesInternalRwma()
|
||||
{
|
||||
var indicator = new RwmaIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RwmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RwmaIndicator { 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 RwmaIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RwmaIndicator { 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 RwmaIndicator_Value_TracksRangeWeightedAverage()
|
||||
{
|
||||
var indicator = new RwmaIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var recordedValues = new List<double>();
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
// Create varying price patterns with varying ranges
|
||||
double open = 100 + i;
|
||||
double high = open + 10 + (i % 5);
|
||||
double low = open - 5;
|
||||
double close = (i % 2 == 0) ? high - 1 : low + 1;
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), open, high, low, close, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
recordedValues.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
// RWMA should produce finite values
|
||||
Assert.True(recordedValues.Count > 0, "Should have recorded values");
|
||||
Assert.All(recordedValues, v => Assert.True(double.IsFinite(v)));
|
||||
|
||||
// RWMA values should be within price range (approximately)
|
||||
double avgValue = recordedValues.Average();
|
||||
Assert.True(avgValue > 90 && avgValue < 200, $"RWMA {avgValue} should be within reasonable price range");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RwmaIndicator_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var indicator5 = new RwmaIndicator { Period = 5 };
|
||||
var indicator20 = new RwmaIndicator { 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;
|
||||
|
||||
indicator5.HistoricalData.AddBar(now.AddMinutes(i), open, high, low, close, 1000);
|
||||
indicator20.HistoricalData.AddBar(now.AddMinutes(i), open, high, low, close, 1000);
|
||||
|
||||
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
|
||||
// Shorter period responds faster to recent prices
|
||||
Assert.NotEqual(val5, val20, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RwmaIndicator_SlidingWindow_DropsOldValues()
|
||||
{
|
||||
var indicator = new RwmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add initial bars with constant price
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double valueAtConstant = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Add bars with higher prices - old low prices should drop out
|
||||
for (int i = 3; i < 6; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 200, 210, 190, 200, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double valueAfterHigh = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Value should have changed significantly as old bars dropped
|
||||
Assert.True(valueAfterHigh > valueAtConstant + 50,
|
||||
$"RWMA should increase as low-price bars drop out: {valueAtConstant} -> {valueAfterHigh}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RwmaIndicator_VolatileBarsHaveMoreWeight()
|
||||
{
|
||||
var indicator = new RwmaIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add bars with varying ranges — volatile bar at close=50, quiet bar at close=150
|
||||
// Volatile bar: range = 40
|
||||
indicator.HistoricalData.AddBar(now, 50, 70, 30, 50, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Quiet bar: range = 2
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 150, 151, 149, 150, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// RWMA should be close to 50 (the volatile bar) rather than 150
|
||||
Assert.True(val < 60, $"RWMA {val} should be weighted toward volatile bar close (50)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Quantower adapter for RWMA (Range Weighted Moving Average).
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public sealed class RwmaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 10, 1, 10000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Rwma _rwma = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"RWMA({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends_FIR/rwma/Rwma.Quantower.cs";
|
||||
|
||||
public RwmaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "RWMA - Range Weighted Moving Average";
|
||||
Description = "Range Weighted Moving Average weights each bar's close by its price range (high - low), giving greater influence to volatile bars.";
|
||||
|
||||
_series = new LineSeries(name: "RWMA", color: Color.Cyan, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_rwma = new Rwma(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _rwma.Update(bar, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _rwma.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,761 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RwmaTests
|
||||
{
|
||||
private readonly GBM _feed;
|
||||
private readonly TBarSeries _bars;
|
||||
|
||||
public RwmaTests()
|
||||
{
|
||||
_feed = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
_bars = new TBarSeries();
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
_bars.Add(_feed.Next());
|
||||
}
|
||||
}
|
||||
|
||||
// ============ A) Constructor Validation ============
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultPeriod_ShouldBe14()
|
||||
{
|
||||
var rwma = new Rwma();
|
||||
Assert.Equal("Rwma(14)", rwma.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithPeriod_ShouldSetName()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
Assert.Equal("Rwma(10)", rwma.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroPeriod_ShouldThrow()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rwma(0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_ShouldThrow()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Rwma(-1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Period1_ShouldNotThrow()
|
||||
{
|
||||
var rwma = new Rwma(1);
|
||||
Assert.Equal("Rwma(1)", rwma.Name);
|
||||
}
|
||||
|
||||
// ============ B) Basic Calculation ============
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsValidTValue()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
var bar = _bars[0];
|
||||
var result = rwma.Update(bar);
|
||||
|
||||
Assert.NotEqual(default, result);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_ShouldBeClosePrice()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
var bar = new TBar(DateTime.UtcNow, 10, 15, 8, 12, 1000);
|
||||
var result = rwma.Update(bar);
|
||||
|
||||
// RWMA of first bar: range=15-8=7, sumCR=12*7=84, sumR=7, RWMA=84/7=12
|
||||
Assert.Equal(12.0, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleBarsSamePrice_ShouldReturnSameRwma()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
// All bars have same close and same range
|
||||
var bar1 = new TBar(DateTime.UtcNow, 95, 105, 95, 100, 100);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 95, 105, 95, 100, 200);
|
||||
var bar3 = new TBar(DateTime.UtcNow.AddMinutes(2), 95, 105, 95, 100, 300);
|
||||
|
||||
rwma.Update(bar1);
|
||||
rwma.Update(bar2);
|
||||
var result = rwma.Update(bar3);
|
||||
|
||||
// All closes = 100, all ranges = 10, so RWMA = (100*10 + 100*10 + 100*10) / (10+10+10) = 100
|
||||
Assert.Equal(100.0, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_RangeWeighting_Works()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
// Bar 1: close=10, range=2 (high=11, low=9)
|
||||
// Bar 2: close=20, range=6 (high=23, low=17)
|
||||
// RWMA = (10*2 + 20*6) / (2+6) = (20 + 120) / 8 = 17.5
|
||||
var bar1 = new TBar(DateTime.UtcNow, 10, 11, 9, 10, 100);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 20, 23, 17, 20, 100);
|
||||
|
||||
rwma.Update(bar1);
|
||||
var result = rwma.Update(bar2);
|
||||
|
||||
Assert.Equal(17.5, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_HighRangeBar_HasMoreInfluence()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
|
||||
// Bar 1: close=10, high range (range=20)
|
||||
var bar1 = new TBar(DateTime.UtcNow, 10, 20, 0, 10, 100);
|
||||
// Bar 2: close=20, low range (range=2)
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 20, 21, 19, 20, 100);
|
||||
|
||||
rwma.Update(bar1);
|
||||
var result = rwma.Update(bar2);
|
||||
|
||||
// RWMA = (10*20 + 20*2) / (20+2) = (200+40)/22 = 10.909...
|
||||
double expected = (10.0 * 20.0 + 20.0 * 2.0) / (20.0 + 2.0);
|
||||
Assert.Equal(expected, result.Value, 10);
|
||||
|
||||
// Should be closer to 10 (the high-range bar) than 20
|
||||
Assert.True(result.Value < 15, "RWMA should be weighted toward high-range bar's close");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SlidingWindow_ShouldDropOldValues()
|
||||
{
|
||||
var rwma = new Rwma(2);
|
||||
// Period = 2, so only last 2 bars count
|
||||
|
||||
// Bar 1: close=10, range=4 (h=12, l=8)
|
||||
var bar1 = new TBar(DateTime.UtcNow, 10, 12, 8, 10, 100);
|
||||
rwma.Update(bar1);
|
||||
|
||||
// Bar 2: close=20, range=4 (h=22, l=18)
|
||||
// RWMA = (10*4 + 20*4) / (4+4) = 120/8 = 15
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 20, 22, 18, 20, 100);
|
||||
rwma.Update(bar2);
|
||||
Assert.Equal(15.0, rwma.Last.Value, 10);
|
||||
|
||||
// Bar 3: close=30, range=4 (h=32, l=28)
|
||||
// Now bar1 drops out: RWMA = (20*4 + 30*4) / (4+4) = 200/8 = 25
|
||||
var bar3 = new TBar(DateTime.UtcNow.AddMinutes(2), 30, 32, 28, 30, 100);
|
||||
var result = rwma.Update(bar3);
|
||||
|
||||
Assert.Equal(25.0, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ZeroRange_DegeneratesToClose()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
// All bars have zero range (high == low == close)
|
||||
var bar1 = new TBar(DateTime.UtcNow, 50, 50, 50, 50, 100);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 60, 60, 60, 60, 100);
|
||||
var bar3 = new TBar(DateTime.UtcNow.AddMinutes(2), 70, 70, 70, 70, 100);
|
||||
|
||||
rwma.Update(bar1);
|
||||
rwma.Update(bar2);
|
||||
var result = rwma.Update(bar3);
|
||||
|
||||
// All ranges = 0, so RWMA degenerates to current close = 70
|
||||
Assert.Equal(70.0, result.Value, 10);
|
||||
}
|
||||
|
||||
// ============ C) State + Bar Correction (isNew) ============
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterPeriodBars_ShouldBeTrue()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
Assert.False(rwma.IsHot);
|
||||
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
rwma.Update(_bars[i]);
|
||||
Assert.False(rwma.IsHot);
|
||||
}
|
||||
|
||||
rwma.Update(_bars[9]);
|
||||
Assert.True(rwma.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_ShouldMatchPeriod()
|
||||
{
|
||||
var rwma = new Rwma(14);
|
||||
Assert.Equal(14, rwma.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_ShouldAdvanceState()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
var bar1 = new TBar(DateTime.UtcNow, 10, 15, 5, 10, 100);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 20, 25, 15, 20, 100);
|
||||
|
||||
rwma.Update(bar1, isNew: true);
|
||||
var result1 = rwma.Last.Value;
|
||||
|
||||
rwma.Update(bar2, isNew: true);
|
||||
var result2 = rwma.Last.Value;
|
||||
|
||||
Assert.NotEqual(result1, result2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_ShouldRollback()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
var bar1 = new TBar(DateTime.UtcNow, 10, 15, 5, 10, 100);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 20, 25, 15, 20, 100);
|
||||
var bar2Updated = new TBar(DateTime.UtcNow.AddMinutes(1), 15, 18, 12, 15, 100);
|
||||
|
||||
rwma.Update(bar1, isNew: true);
|
||||
rwma.Update(bar2, isNew: true);
|
||||
var afterBar2 = rwma.Last.Value;
|
||||
|
||||
// Correct bar2 with updated values
|
||||
rwma.Update(bar2Updated, isNew: false);
|
||||
var afterCorrection = rwma.Last.Value;
|
||||
|
||||
Assert.NotEqual(afterBar2, afterCorrection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_ShouldRestoreState()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
|
||||
// Process first 10 bars
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
rwma.Update(_bars[i], isNew: true);
|
||||
}
|
||||
_ = rwma.Last.Value;
|
||||
|
||||
// Process bar 11
|
||||
rwma.Update(_bars[10], isNew: true);
|
||||
var valueAfter11 = rwma.Last.Value;
|
||||
|
||||
// Correct bar 11 multiple times with same data
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
rwma.Update(_bars[10], isNew: false);
|
||||
}
|
||||
var valueAfterCorrections = rwma.Last.Value;
|
||||
|
||||
// Should get same result as after first processing of bar 11
|
||||
Assert.Equal(valueAfter11, valueAfterCorrections, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ShouldClearState()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
rwma.Update(_bars[i]);
|
||||
}
|
||||
Assert.True(rwma.IsHot);
|
||||
|
||||
rwma.Reset();
|
||||
|
||||
Assert.False(rwma.IsHot);
|
||||
Assert.Equal(default, rwma.Last);
|
||||
}
|
||||
|
||||
// ============ D) Warmup/Convergence ============
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsExactlyAtPeriod()
|
||||
{
|
||||
var rwma = new Rwma(5);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
rwma.Update(_bars[i]);
|
||||
Assert.False(rwma.IsHot, $"IsHot should be false at bar {i}");
|
||||
}
|
||||
|
||||
rwma.Update(_bars[4]);
|
||||
Assert.True(rwma.IsHot, "IsHot should be true at bar 4 (5th bar)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_DependsOnPeriod()
|
||||
{
|
||||
Assert.Equal(5, new Rwma(5).WarmupPeriod);
|
||||
Assert.Equal(20, new Rwma(20).WarmupPeriod);
|
||||
Assert.Equal(100, new Rwma(100).WarmupPeriod);
|
||||
}
|
||||
|
||||
// ============ E) Robustness (NaN/Infinity) ============
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_ShouldUseLastValidValue()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
|
||||
// First bar establishes valid values
|
||||
var bar1 = new TBar(DateTime.UtcNow, 10, 15, 8, 12, 1000);
|
||||
rwma.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 = rwma.Update(bar2);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_ShouldUseLastValidValue()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 10, 15, 8, 12, 1000);
|
||||
rwma.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity);
|
||||
var result = rwma.Update(bar2);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BatchNaN_ShouldRemainFinite()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
|
||||
// Establish valid state
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
rwma.Update(_bars[i]);
|
||||
}
|
||||
|
||||
// Send multiple NaN bars
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var nanBar = new TBar(DateTime.UtcNow.AddMinutes(20 + i),
|
||||
double.NaN, double.NaN, double.NaN, double.NaN, double.NaN);
|
||||
var result = rwma.Update(nanBar);
|
||||
Assert.True(double.IsFinite(result.Value), $"NaN bar {i} produced non-finite result");
|
||||
}
|
||||
}
|
||||
|
||||
// ============ F) Consistency (4 API modes) ============
|
||||
|
||||
[Fact]
|
||||
public void Streaming_ShouldMatchBatch()
|
||||
{
|
||||
int period = 14;
|
||||
|
||||
// Streaming
|
||||
var rwma = new Rwma(period);
|
||||
var streamingResults = new List<double>();
|
||||
foreach (var bar in _bars)
|
||||
{
|
||||
streamingResults.Add(rwma.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Rwma.Batch(_bars, period);
|
||||
|
||||
// Compare last 100 values
|
||||
for (int i = _bars.Count - 100; i < _bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], streamingResults[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_TBarSeries_ShouldMatchSpan()
|
||||
{
|
||||
int period = 14;
|
||||
|
||||
var batchResult = Rwma.Batch(_bars, period);
|
||||
|
||||
var close = _bars.Close.Values.ToArray();
|
||||
var high = _bars.High.Values.ToArray();
|
||||
var low = _bars.Low.Values.ToArray();
|
||||
var spanOutput = new double[_bars.Count];
|
||||
Rwma.Batch(close, high, low, spanOutput, period);
|
||||
|
||||
for (int i = 0; i < _bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], spanOutput[i], 12);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Eventing_ShouldMatchStreaming()
|
||||
{
|
||||
int period = 14;
|
||||
|
||||
// Streaming
|
||||
var rwma1 = new Rwma(period);
|
||||
var streamingResults = new List<double>();
|
||||
foreach (var bar in _bars)
|
||||
{
|
||||
streamingResults.Add(rwma1.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Event-based
|
||||
var rwma2 = new Rwma(period);
|
||||
var eventResults = new List<double>();
|
||||
rwma2.Pub += (object? sender, in TValueEventArgs args) => eventResults.Add(args.Value.Value);
|
||||
foreach (var bar in _bars)
|
||||
{
|
||||
rwma2.Update(bar);
|
||||
}
|
||||
|
||||
Assert.Equal(streamingResults.Count, eventResults.Count);
|
||||
for (int i = 0; i < streamingResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], eventResults[i], 12);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ G) Span API Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_MismatchedLengths_ShouldThrow()
|
||||
{
|
||||
var close = new double[100];
|
||||
var high = new double[99]; // Mismatched
|
||||
var low = new double[100];
|
||||
var output = new double[100];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Rwma.Batch(close, high, low, output, 10));
|
||||
Assert.Equal("high", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_OutputLengthMismatch_ShouldThrow()
|
||||
{
|
||||
var close = new double[100];
|
||||
var high = new double[100];
|
||||
var low = new double[100];
|
||||
var output = new double[50]; // Mismatched
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Rwma.Batch(close, high, low, output, 10));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_ZeroPeriod_ShouldThrow()
|
||||
{
|
||||
var close = new double[100];
|
||||
var high = new double[100];
|
||||
var low = new double[100];
|
||||
var output = new double[100];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Rwma.Batch(close, high, low, output, 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_NegativePeriod_ShouldThrow()
|
||||
{
|
||||
var close = new double[100];
|
||||
var high = new double[100];
|
||||
var low = new double[100];
|
||||
var output = new double[100];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Rwma.Batch(close, high, low, output, -1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_NaN_ShouldNotPropagate()
|
||||
{
|
||||
var close = new double[] { 10, 20, double.NaN, 40, 50 };
|
||||
var high = new double[] { 15, 25, double.NaN, 45, 55 };
|
||||
var low = new double[] { 5, 15, double.NaN, 35, 45 };
|
||||
var output = new double[5];
|
||||
|
||||
Rwma.Batch(close, high, low, output, 3);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"Output at index {i} is not finite: {output[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_LargeData_ShouldNotOverflow()
|
||||
{
|
||||
// Test with period > StackallocThreshold (256)
|
||||
int period = 300;
|
||||
int len = 500;
|
||||
var close = new double[len];
|
||||
var high = new double[len];
|
||||
var low = new double[len];
|
||||
var output = new double[len];
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
close[i] = 100 + i;
|
||||
high[i] = 100 + i + 5;
|
||||
low[i] = 100 + i - 5;
|
||||
}
|
||||
|
||||
Rwma.Batch(close, high, low, output, period);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"Output at index {i} is not finite");
|
||||
}
|
||||
}
|
||||
|
||||
// ============ H) Chainability / Events ============
|
||||
|
||||
[Fact]
|
||||
public void Pub_ShouldFireOnUpdate()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
int eventCount = 0;
|
||||
|
||||
rwma.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
rwma.Update(_bars[0]);
|
||||
rwma.Update(_bars[1]);
|
||||
|
||||
Assert.Equal(2, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventArgs_ShouldContainCorrectValue()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
TValue? lastEventValue = null;
|
||||
|
||||
rwma.Pub += (object? sender, in TValueEventArgs args) => lastEventValue = args.Value;
|
||||
|
||||
var result = rwma.Update(_bars[0]);
|
||||
Assert.NotNull(lastEventValue);
|
||||
Assert.Equal(result.Value, lastEventValue.Value.Value, 12);
|
||||
}
|
||||
|
||||
// ============ TValue Input Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_ShouldWork()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
var result = rwma.Update(input);
|
||||
|
||||
// With TValue, high=low=close → range=0, degenerates to close
|
||||
Assert.Equal(100.0, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_MultipleInputs_DegeneratesToClose()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
|
||||
// TValue input: range always 0, so always degenerates to current close
|
||||
rwma.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
var result = rwma.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 200.0));
|
||||
|
||||
// All ranges 0 → fallback to current close = 200
|
||||
Assert.Equal(200.0, result.Value, 10);
|
||||
}
|
||||
|
||||
// ============ Batch/Series Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Update_TBarSeries_ShouldReturnTSeries()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
var result = rwma.Update(_bars);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(_bars.Count, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Static_ShouldReturnTSeries()
|
||||
{
|
||||
var result = Rwma.Batch(_bars, 10);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(_bars.Count, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Static_WithDifferentPeriods_ShouldWork()
|
||||
{
|
||||
var result14 = Rwma.Batch(_bars, 14);
|
||||
var result50 = Rwma.Batch(_bars, 50);
|
||||
|
||||
Assert.NotNull(result14);
|
||||
Assert.NotNull(result50);
|
||||
Assert.Equal(_bars.Count, result14.Count);
|
||||
Assert.Equal(_bars.Count, result50.Count);
|
||||
}
|
||||
|
||||
// ============ TSeries Calculate Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_ShouldReturnTSeriesAndIndicator()
|
||||
{
|
||||
var (results, indicator) = Rwma.Calculate(_bars, 14);
|
||||
|
||||
Assert.NotNull(results);
|
||||
Assert.Equal(_bars.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_TSeries_ShouldWork()
|
||||
{
|
||||
var sourceSeries = _bars.Close;
|
||||
var result = Rwma.Batch(sourceSeries, 20);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(sourceSeries.Count, result.Count);
|
||||
}
|
||||
|
||||
// ============ Prime Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Prime_ShouldInitializeState()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
rwma.Prime(_bars);
|
||||
|
||||
Assert.True(rwma.IsHot);
|
||||
Assert.True(double.IsFinite(rwma.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_ThenUpdate_ShouldContinueCorrectly()
|
||||
{
|
||||
var rwma1 = new Rwma(10);
|
||||
var rwma2 = new Rwma(10);
|
||||
|
||||
// rwma1: process all bars
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
rwma1.Update(_bars[i]);
|
||||
}
|
||||
|
||||
// rwma2: prime with first 50, then stream remaining
|
||||
var primeBars = new TBarSeries();
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
primeBars.Add(_bars[i]);
|
||||
}
|
||||
rwma2.Prime(primeBars);
|
||||
for (int i = 50; i < 100; i++)
|
||||
{
|
||||
rwma2.Update(_bars[i]);
|
||||
}
|
||||
|
||||
// Both should produce the same result
|
||||
Assert.Equal(rwma1.Last.Value, rwma2.Last.Value, 10);
|
||||
}
|
||||
|
||||
// ============ Algorithm-Specific Tests ============
|
||||
|
||||
[Fact]
|
||||
public void RangeWeighting_VolatileBarHasMoreWeight()
|
||||
{
|
||||
var rwma = new Rwma(10);
|
||||
|
||||
// Bar with large range (volatile) at close=50
|
||||
var volatileBar = new TBar(DateTime.UtcNow, 50, 70, 30, 50, 100); // range=40
|
||||
// Bar with small range (quiet) at close=100
|
||||
var quietBar = new TBar(DateTime.UtcNow.AddMinutes(1), 100, 101, 99, 100, 100); // range=2
|
||||
|
||||
rwma.Update(volatileBar);
|
||||
var result = rwma.Update(quietBar);
|
||||
|
||||
// RWMA = (50*40 + 100*2) / (40+2) = (2000+200)/42 = 52.38...
|
||||
double expected = (50.0 * 40.0 + 100.0 * 2.0) / 42.0;
|
||||
Assert.Equal(expected, result.Value, 10);
|
||||
|
||||
// Should be much closer to 50 than 100
|
||||
Assert.True(result.Value < 60, "RWMA should strongly lean toward the volatile bar's close");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StablePrice_ConstantRange_ShouldReturnSma()
|
||||
{
|
||||
var rwma = new Rwma(5);
|
||||
|
||||
// When all bars have the same range, RWMA reduces to SMA of closes
|
||||
// because weights are all equal
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 10, 20, 30, 40, 50 };
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
// All bars have range = 10
|
||||
var bar = new TBar(now.AddMinutes(i), closes[i], closes[i] + 5, closes[i] - 5, closes[i], 100);
|
||||
rwma.Update(bar);
|
||||
}
|
||||
|
||||
// When all ranges equal, RWMA = SMA = (10+20+30+40+50)/5 = 30
|
||||
Assert.Equal(30.0, rwma.Last.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Period1_ShouldReturnClose()
|
||||
{
|
||||
var rwma = new Rwma(1);
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow, 50, 60, 40, 55, 100);
|
||||
var result = rwma.Update(bar);
|
||||
|
||||
// Period 1: only current bar, RWMA = close * range / range = close
|
||||
Assert.Equal(55.0, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvexCombination_NeverExceedsPriceRange()
|
||||
{
|
||||
var rwma = new Rwma(20);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
results.Add(rwma.Update(_bars[i]).Value);
|
||||
}
|
||||
|
||||
// Find min/max close in last 20 bars for the last few results
|
||||
for (int i = 80; i < 100; i++)
|
||||
{
|
||||
double minClose = double.MaxValue;
|
||||
double maxClose = double.MinValue;
|
||||
for (int j = i - 19; j <= i; j++)
|
||||
{
|
||||
double c = _bars[j].Close;
|
||||
if (c < minClose)
|
||||
{
|
||||
minClose = c;
|
||||
}
|
||||
if (c > maxClose)
|
||||
{
|
||||
maxClose = c;
|
||||
}
|
||||
}
|
||||
|
||||
// RWMA is a convex combination — should be within [minClose, maxClose]
|
||||
Assert.True(results[i] >= minClose - 1e-9 && results[i] <= maxClose + 1e-9,
|
||||
$"RWMA at {i} ({results[i]}) should be within [{minClose}, {maxClose}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RwmaValidationTests
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
|
||||
public RwmaValidationTests()
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
}
|
||||
|
||||
// ============ External Library Validation ============
|
||||
// RWMA is not available in Skender, TA-Lib, Tulip, or Ooples.
|
||||
// Validation focuses on internal consistency and algorithm correctness.
|
||||
|
||||
[Fact]
|
||||
public void Rwma_NotAvailable_Skender()
|
||||
{
|
||||
Assert.True(true, "RWMA is not available in Skender.Stock.Indicators");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rwma_NotAvailable_TaLib()
|
||||
{
|
||||
Assert.True(true, "RWMA is not available in TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rwma_NotAvailable_Tulip()
|
||||
{
|
||||
Assert.True(true, "RWMA is not available in Tulip");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rwma_NotAvailable_Ooples()
|
||||
{
|
||||
Assert.True(true, "RWMA is not available in OoplesFinance");
|
||||
}
|
||||
|
||||
// ============ Internal Consistency Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Rwma_Streaming_Matches_Batch()
|
||||
{
|
||||
int period = 14;
|
||||
|
||||
// Streaming
|
||||
var rwma = new Rwma(period);
|
||||
var streamingValues = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
streamingValues.Add(rwma.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Rwma.Batch(_data.Bars, period);
|
||||
var batchValues = batchResult.Values.ToArray();
|
||||
|
||||
ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rwma_Span_Matches_Streaming()
|
||||
{
|
||||
int period = 14;
|
||||
|
||||
// Streaming
|
||||
var rwma = new Rwma(period);
|
||||
var streamingValues = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
streamingValues.Add(rwma.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Span
|
||||
var close = _data.Bars.Close.Values.ToArray();
|
||||
var high = _data.Bars.High.Values.ToArray();
|
||||
var low = _data.Bars.Low.Values.ToArray();
|
||||
var spanValues = new double[close.Length];
|
||||
Rwma.Batch(close, high, low, spanValues, period);
|
||||
|
||||
ValidationHelper.VerifyData(streamingValues.ToArray(), spanValues, 0, 100, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rwma_Batch_Matches_Span()
|
||||
{
|
||||
int period = 14;
|
||||
|
||||
// Batch
|
||||
var batchResult = Rwma.Batch(_data.Bars, period);
|
||||
var batchValues = batchResult.Values.ToArray();
|
||||
|
||||
// Span
|
||||
var close = _data.Bars.Close.Values.ToArray();
|
||||
var high = _data.Bars.High.Values.ToArray();
|
||||
var low = _data.Bars.Low.Values.ToArray();
|
||||
var spanValues = new double[close.Length];
|
||||
Rwma.Batch(close, high, low, spanValues, period);
|
||||
|
||||
// Batch and Span use identical code path, should match exactly
|
||||
ValidationHelper.VerifyData(batchValues, spanValues, 0, 100, 1e-12);
|
||||
}
|
||||
|
||||
// ============ Algorithm Correctness Tests ============
|
||||
|
||||
[Fact]
|
||||
public void Rwma_Algorithm_Correctness_ManualCalculation()
|
||||
{
|
||||
// Manual calculation to verify algorithm correctness
|
||||
var bars = new TBarSeries();
|
||||
|
||||
// Bar 0: close=10, high=15, low=5 → range=10
|
||||
// Bar 1: close=20, high=24, low=18 → range=6
|
||||
// Bar 2: close=30, high=35, low=25 → range=10
|
||||
bars.Add(new TBar(DateTime.UtcNow, 10, 15, 5, 10, 100));
|
||||
bars.Add(new TBar(DateTime.UtcNow.AddMinutes(1), 20, 24, 18, 20, 100));
|
||||
bars.Add(new TBar(DateTime.UtcNow.AddMinutes(2), 30, 35, 25, 30, 100));
|
||||
|
||||
var rwma = new Rwma(10); // Period larger than data
|
||||
var results = new List<double>();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
results.Add(rwma.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Bar 0: RWMA = 10*10 / 10 = 10
|
||||
Assert.Equal(10.0, results[0], 6);
|
||||
|
||||
// Bar 1: RWMA = (10*10 + 20*6) / (10+6) = (100+120)/16 = 13.75
|
||||
double expectedBar1 = (10.0 * 10.0 + 20.0 * 6.0) / 16.0;
|
||||
Assert.Equal(expectedBar1, results[1], 6);
|
||||
|
||||
// Bar 2: RWMA = (10*10 + 20*6 + 30*10) / (10+6+10) = (100+120+300)/26 = 20.0
|
||||
double expectedBar2 = (10.0 * 10.0 + 20.0 * 6.0 + 30.0 * 10.0) / 26.0;
|
||||
Assert.Equal(expectedBar2, results[2], 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rwma_Algorithm_Correctness_SlidingWindow()
|
||||
{
|
||||
// Verify sliding window drops old values correctly
|
||||
var rwma = new Rwma(2); // Period = 2
|
||||
|
||||
// Bar 0: close=10, range=10 (h=15, l=5)
|
||||
rwma.Update(new TBar(DateTime.UtcNow, 10, 15, 5, 10, 100));
|
||||
Assert.Equal(10.0, rwma.Last.Value, 6);
|
||||
|
||||
// Bar 1: close=20, range=6 (h=23, l=17)
|
||||
// RWMA = (10*10 + 20*6) / (10+6) = 220/16 = 13.75
|
||||
rwma.Update(new TBar(DateTime.UtcNow.AddMinutes(1), 20, 23, 17, 20, 100));
|
||||
Assert.Equal(13.75, rwma.Last.Value, 6);
|
||||
|
||||
// Bar 2: close=30, range=10 (h=35, l=25)
|
||||
// Now bar0 drops out: RWMA = (20*6 + 30*10) / (6+10) = (120+300)/16 = 26.25
|
||||
rwma.Update(new TBar(DateTime.UtcNow.AddMinutes(2), 30, 35, 25, 30, 100));
|
||||
Assert.Equal(26.25, rwma.Last.Value, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rwma_Algorithm_Correctness_RangeWeighting()
|
||||
{
|
||||
// Verify range weighting: high-range bars have more influence
|
||||
var rwma = new Rwma(10);
|
||||
|
||||
// Two bars: one with high range at low price, one with low range at high price
|
||||
rwma.Update(new TBar(DateTime.UtcNow, 10, 60, 10, 10, 100)); // range=50
|
||||
var result = rwma.Update(new TBar(DateTime.UtcNow.AddMinutes(1), 100, 101, 99, 100, 100)); // range=2
|
||||
|
||||
// RWMA = (10*50 + 100*2) / (50+2) = (500+200)/52 = 13.46...
|
||||
double expected = (10.0 * 50.0 + 100.0 * 2.0) / 52.0;
|
||||
Assert.Equal(expected, result.Value, 6);
|
||||
|
||||
// RWMA should be much closer to 10 than to 100
|
||||
Assert.True(result.Value < 20, "RWMA should be weighted toward high-range price");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rwma_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var rwma10 = new Rwma(10);
|
||||
var rwma20 = new Rwma(20);
|
||||
var rwma50 = new Rwma(50);
|
||||
|
||||
var results10 = new List<double>();
|
||||
var results20 = new List<double>();
|
||||
var results50 = new List<double>();
|
||||
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
results10.Add(rwma10.Update(bar).Value);
|
||||
results20.Add(rwma20.Update(bar).Value);
|
||||
results50.Add(rwma50.Update(bar).Value);
|
||||
}
|
||||
|
||||
// After sufficient bars, different periods should produce different results
|
||||
int checkIndex = 60;
|
||||
bool anyDifferent = Math.Abs(results10[checkIndex] - results20[checkIndex]) > 1e-6 ||
|
||||
Math.Abs(results20[checkIndex] - results50[checkIndex]) > 1e-6;
|
||||
|
||||
Assert.True(anyDifferent, "Different periods should produce different RWMA values");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rwma_StableWithConstantPrice()
|
||||
{
|
||||
// RWMA should remain stable when close price is constant (regardless of range)
|
||||
var rwma = new Rwma(10);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
// Close always 50, but varying ranges
|
||||
double range = 5 + (i % 10);
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 50, 50 + range, 50 - range, 50, 1000);
|
||||
results.Add(rwma.Update(bar).Value);
|
||||
}
|
||||
|
||||
// All RWMA values should be 50 (constant close, varying range)
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
Assert.Equal(50.0, results[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rwma_ZeroRange_DegeneratesToCurrentClose()
|
||||
{
|
||||
// When all ranges are zero, RWMA should return current close
|
||||
var rwma = new Rwma(10);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double close = 100 + i;
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), close, close, close, close, 100);
|
||||
var result = rwma.Update(bar);
|
||||
|
||||
Assert.Equal(close, result.Value, 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rwma_EqualRanges_ReducesToSma()
|
||||
{
|
||||
// When all ranges are equal, RWMA = SMA of closes
|
||||
var rwma = new Rwma(3);
|
||||
|
||||
// Three bars with equal range (10) but different closes
|
||||
rwma.Update(new TBar(DateTime.UtcNow, 10, 15, 5, 10, 100)); // range=10
|
||||
rwma.Update(new TBar(DateTime.UtcNow.AddMinutes(1), 20, 25, 15, 20, 100)); // range=10
|
||||
rwma.Update(new TBar(DateTime.UtcNow.AddMinutes(2), 30, 35, 25, 30, 100)); // range=10
|
||||
|
||||
// RWMA = (10*10 + 20*10 + 30*10) / (10+10+10) = 600/30 = 20 = SMA(10,20,30)
|
||||
Assert.Equal(20.0, rwma.Last.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rwma_ResponsiveToPriceChanges()
|
||||
{
|
||||
// Shorter period RWMA should track price more closely
|
||||
var rwmaShort = new Rwma(5);
|
||||
var rwmaLong = new Rwma(50);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double close = i;
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), close, close + 5, close - 5, close, 1000);
|
||||
rwmaShort.Update(bar);
|
||||
rwmaLong.Update(bar);
|
||||
}
|
||||
|
||||
// Short period RWMA should be closer to current price (99)
|
||||
double shortDiff = Math.Abs(rwmaShort.Last.Value - 99);
|
||||
double longDiff = Math.Abs(rwmaLong.Last.Value - 99);
|
||||
|
||||
Assert.True(shortDiff < longDiff, "Short period RWMA should track price more closely");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rwma_ConvexCombination_OutputWithinPriceRange()
|
||||
{
|
||||
// RWMA is a convex combination, so output must be within [min, max] of closes in window
|
||||
var rwma = new Rwma(10);
|
||||
var closes = new List<double>();
|
||||
var results = new List<double>();
|
||||
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
closes.Add(bar.Close);
|
||||
results.Add(rwma.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Check after warmup
|
||||
for (int i = 10; i < 200; i++)
|
||||
{
|
||||
double minClose = double.MaxValue;
|
||||
double maxClose = double.MinValue;
|
||||
for (int j = i - 9; j <= i; j++)
|
||||
{
|
||||
if (closes[j] < minClose)
|
||||
{
|
||||
minClose = closes[j];
|
||||
}
|
||||
if (closes[j] > maxClose)
|
||||
{
|
||||
maxClose = closes[j];
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(results[i] >= minClose - 1e-9 && results[i] <= maxClose + 1e-9,
|
||||
$"RWMA at {i} ({results[i]}) should be within [{minClose}, {maxClose}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// RWMA: Range Weighted Moving Average
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Weights each bar's contribution by its price range (high - low), giving
|
||||
/// greater influence to volatile bars and less to narrow-range bars.
|
||||
/// <c>RWMA = Σ(close_i × range_i) / Σ(range_i)</c> where <c>range_i = max(high_i - low_i, 0)</c>.
|
||||
///
|
||||
/// Requires TBar (OHLC) inputs. When all bars have zero range the output
|
||||
/// degenerates to the current close price.
|
||||
///
|
||||
/// O(1) per bar via circular buffers with running sums.
|
||||
/// </remarks>
|
||||
/// <seealso href="Rwma.md">Detailed documentation</seealso>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Rwma : ITValuePublisher
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double SumCR, double SumR, int Index, int Head, int Count, int SyncCounter)
|
||||
{
|
||||
public static State New() => new() { SumCR = 0, SumR = 0, Index = 0, Head = 0, Count = 0, SyncCounter = 0 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resync interval to limit floating-point drift in running sums.
|
||||
/// Full recalculation every N bars.
|
||||
/// </summary>
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
private readonly int _period;
|
||||
private readonly double[] _closeBuffer;
|
||||
private readonly double[] _rangeBuffer;
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
private double _lastValidClose;
|
||||
private double _lastValidHigh;
|
||||
private double _lastValidLow;
|
||||
private double _p_lastValidClose;
|
||||
private double _p_lastValidHigh;
|
||||
private double _p_lastValidLow;
|
||||
private double _p_bufferClose;
|
||||
private double _p_bufferRange;
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Current RWMA 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>
|
||||
#pragma warning disable S2325
|
||||
public int WarmupPeriod => _period;
|
||||
#pragma warning restore S2325
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RWMA indicator.
|
||||
/// </summary>
|
||||
/// <param name="period">Lookback period. Must be >= 1.</param>
|
||||
public Rwma(int period = 14)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be >= 1", nameof(period));
|
||||
}
|
||||
|
||||
_period = period;
|
||||
_closeBuffer = new double[period];
|
||||
_rangeBuffer = new double[period];
|
||||
_state = State.New();
|
||||
_p_state = State.New();
|
||||
Name = $"Rwma({period})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the indicator state.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_state = State.New();
|
||||
_p_state = State.New();
|
||||
Array.Clear(_closeBuffer);
|
||||
Array.Clear(_rangeBuffer);
|
||||
_lastValidClose = 0;
|
||||
_lastValidHigh = 0;
|
||||
_lastValidLow = 0;
|
||||
_p_lastValidClose = 0;
|
||||
_p_lastValidHigh = 0;
|
||||
_p_lastValidLow = 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 sums from buffer to eliminate accumulated floating-point drift.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ResyncRunningTotals(ref State s)
|
||||
{
|
||||
double sumCR = 0;
|
||||
double sumR = 0;
|
||||
|
||||
for (int i = 0; i < _period; i++)
|
||||
{
|
||||
double c = _closeBuffer[i];
|
||||
double r = _rangeBuffer[i];
|
||||
sumCR += c * r;
|
||||
sumR += r;
|
||||
}
|
||||
|
||||
s.SumCR = sumCR;
|
||||
s.SumR = sumR;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates RWMA with a TBar input (uses close, high, low).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
return UpdateInternal(input.Time, input.Close, input.High, input.Low, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates RWMA with a TValue input (uses value as close, range = 0).
|
||||
/// With zero range all bars have equal weight, degenerating to SMA.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
// When given a single value, high = low = close → range = 0
|
||||
// All weights are 0, so fallback to current close
|
||||
return UpdateInternal(input.Time, input.Value, input.Value, input.Value, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates RWMA for an entire bar series.
|
||||
/// </summary>
|
||||
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 close, double high, double low, bool isNew)
|
||||
{
|
||||
var s = _state;
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
_p_lastValidClose = _lastValidClose;
|
||||
_p_lastValidHigh = _lastValidHigh;
|
||||
_p_lastValidLow = _lastValidLow;
|
||||
_p_bufferClose = _closeBuffer[s.Head];
|
||||
_p_bufferRange = _rangeBuffer[s.Head];
|
||||
}
|
||||
else
|
||||
{
|
||||
s = _p_state;
|
||||
_state = _p_state;
|
||||
_lastValidClose = _p_lastValidClose;
|
||||
_lastValidHigh = _p_lastValidHigh;
|
||||
_lastValidLow = _p_lastValidLow;
|
||||
_closeBuffer[s.Head] = _p_bufferClose;
|
||||
_rangeBuffer[s.Head] = _p_bufferRange;
|
||||
}
|
||||
|
||||
double currentClose = GetValidValue(close, ref _lastValidClose);
|
||||
double currentHigh = GetValidValue(high, ref _lastValidHigh);
|
||||
double currentLow = GetValidValue(low, ref _lastValidLow);
|
||||
double currentRange = Math.Max(currentHigh - currentLow, 0.0);
|
||||
|
||||
// Remove old values from circular buffer
|
||||
double oldClose = _closeBuffer[s.Head];
|
||||
double oldRange = _rangeBuffer[s.Head];
|
||||
|
||||
if (s.Count >= _period)
|
||||
{
|
||||
s.SumCR -= oldClose * oldRange;
|
||||
s.SumR -= oldRange;
|
||||
}
|
||||
|
||||
// Add new values
|
||||
s.SumCR += currentClose * currentRange;
|
||||
s.SumR += currentRange;
|
||||
|
||||
// Store in circular buffer
|
||||
_closeBuffer[s.Head] = currentClose;
|
||||
_rangeBuffer[s.Head] = currentRange;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate RWMA: Σ(close × range) / Σ(range)
|
||||
// When all ranges are zero, fall back to current close
|
||||
double rwma = s.SumR > double.Epsilon ? s.SumCR / s.SumR : currentClose;
|
||||
|
||||
_state = s;
|
||||
|
||||
Last = new TValue(time, rwma);
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
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 TBarSeries.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TBarSeries source, int period = 14)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var t = source.Open.Times.ToArray();
|
||||
var v = new double[source.Count];
|
||||
|
||||
Batch(source.Close.Values, source.High.Values, source.Low.Values, v, period);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static calculation for TSeries (single-valued, range = 0 → degenerates to SMA).
|
||||
/// </summary>
|
||||
public static TSeries Batch(TSeries source, int period = 14)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var t = source.Times.ToArray();
|
||||
var v = new double[source.Count];
|
||||
|
||||
// No high/low available — use close for high and low → range = 0, so always fallback to close
|
||||
Batch(source.Values, source.Values, source.Values, v, period);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zero-allocation span-based calculation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public static void Batch(ReadOnlySpan<double> close, ReadOnlySpan<double> high, ReadOnlySpan<double> low, Span<double> output, int period = 14)
|
||||
{
|
||||
if (close.Length != high.Length || close.Length != low.Length)
|
||||
{
|
||||
throw new ArgumentException("Close, High, and Low spans must be of the same length", nameof(high));
|
||||
}
|
||||
|
||||
if (close.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 = close.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int StackallocThreshold = 256;
|
||||
double[]? rentedClose = null;
|
||||
double[]? rentedRange = null;
|
||||
scoped Span<double> closeBuffer;
|
||||
scoped Span<double> rangeBuffer;
|
||||
|
||||
if (period <= StackallocThreshold)
|
||||
{
|
||||
closeBuffer = stackalloc double[period];
|
||||
rangeBuffer = stackalloc double[period];
|
||||
}
|
||||
else
|
||||
{
|
||||
rentedClose = System.Buffers.ArrayPool<double>.Shared.Rent(period);
|
||||
rentedRange = System.Buffers.ArrayPool<double>.Shared.Rent(period);
|
||||
closeBuffer = rentedClose.AsSpan(0, period);
|
||||
rangeBuffer = rentedRange.AsSpan(0, period);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
closeBuffer.Clear();
|
||||
rangeBuffer.Clear();
|
||||
|
||||
double sumCR = 0;
|
||||
double sumR = 0;
|
||||
double lastValidClose = 0;
|
||||
double lastValidHigh = 0;
|
||||
double lastValidLow = 0;
|
||||
int head = 0;
|
||||
int count = 0;
|
||||
|
||||
// Find first valid values
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
if (double.IsFinite(close[k])) { lastValidClose = close[k]; break; }
|
||||
}
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
if (double.IsFinite(high[k])) { lastValidHigh = high[k]; break; }
|
||||
}
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
if (double.IsFinite(low[k])) { lastValidLow = low[k]; break; }
|
||||
}
|
||||
|
||||
int syncCounter = 0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double currentClose = double.IsFinite(close[i]) ? close[i] : lastValidClose;
|
||||
double currentHigh = double.IsFinite(high[i]) ? high[i] : lastValidHigh;
|
||||
double currentLow = double.IsFinite(low[i]) ? low[i] : lastValidLow;
|
||||
|
||||
if (double.IsFinite(close[i]))
|
||||
{
|
||||
lastValidClose = close[i];
|
||||
}
|
||||
if (double.IsFinite(high[i]))
|
||||
{
|
||||
lastValidHigh = high[i];
|
||||
}
|
||||
if (double.IsFinite(low[i]))
|
||||
{
|
||||
lastValidLow = low[i];
|
||||
}
|
||||
|
||||
double currentRange = Math.Max(currentHigh - currentLow, 0.0);
|
||||
|
||||
// Remove old values from circular buffer
|
||||
double oldClose = closeBuffer[head];
|
||||
double oldRange = rangeBuffer[head];
|
||||
|
||||
if (count >= period)
|
||||
{
|
||||
sumCR -= oldClose * oldRange;
|
||||
sumR -= oldRange;
|
||||
}
|
||||
|
||||
// Add new values
|
||||
sumCR += currentClose * currentRange;
|
||||
sumR += currentRange;
|
||||
|
||||
// Store in circular buffer
|
||||
closeBuffer[head] = currentClose;
|
||||
rangeBuffer[head] = currentRange;
|
||||
|
||||
head = (head + 1) % period;
|
||||
|
||||
if (count < period)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
// Periodic resync
|
||||
syncCounter++;
|
||||
if (syncCounter >= ResyncInterval && count >= period)
|
||||
{
|
||||
syncCounter = 0;
|
||||
sumCR = 0;
|
||||
sumR = 0;
|
||||
for (int j = 0; j < period; j++)
|
||||
{
|
||||
sumCR += closeBuffer[j] * rangeBuffer[j];
|
||||
sumR += rangeBuffer[j];
|
||||
}
|
||||
}
|
||||
|
||||
output[i] = sumR > double.Epsilon ? sumCR / sumR : currentClose;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedClose != null)
|
||||
{
|
||||
System.Buffers.ArrayPool<double>.Shared.Return(rentedClose);
|
||||
}
|
||||
if (rentedRange != null)
|
||||
{
|
||||
System.Buffers.ArrayPool<double>.Shared.Return(rentedRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Results, Rwma Indicator) Calculate(TBarSeries source, int period = 14)
|
||||
{
|
||||
var indicator = new Rwma(period);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user