mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-04 04:07:42 +00:00
DWMA Indicator implementation and tests
This commit is contained in:
@@ -11,6 +11,7 @@ This document defines the strict standards for creating high-quality technical i
|
||||
* **Bar Correction:** Support intra-bar updates via the `isNew` parameter. The indicator must be able to rollback the last update and apply a new value for the same timestamp.
|
||||
* **Robustness:** Handle `NaN` and `Infinity` gracefully using last-valid-value substitution. Never propagate invalid values.
|
||||
* **Reactive:** Implement `ITValuePublisher` to support event-driven architectures.
|
||||
* **Time Handling:** Always use `DateTime.UtcNow` instead of `DateTime.Now` to ensure consistent time handling across timezones.
|
||||
|
||||
## 2. File Structure
|
||||
|
||||
@@ -91,6 +92,7 @@ Each indicator resides in its own directory such as `lib/trends/`, `lib/indicato
|
||||
### Unit Tests (`[Name].Tests.cs`)
|
||||
|
||||
* **Framework:** xUnit
|
||||
* **Data Generation:** Use `GBM` (Geometric Brownian Motion) for generating realistic test data. Avoid using `System.Random` directly.
|
||||
* **Coverage:**
|
||||
|
||||
* Constructor validation (invalid params).
|
||||
@@ -155,7 +157,7 @@ Template structure:
|
||||
|
||||
## 9. Checklist for New Indicators
|
||||
|
||||
* [ ] **Source Material:** Sourced algorithm and docs from `mihakralj/pinescript`?
|
||||
* [ ] **Source Material:** Sourced algorithm and docs from `mihakralj/pinescript` or `mihakralj/quantalib`?
|
||||
* [ ] **File Structure:** Created all 6 required files?
|
||||
* [ ] **Constructor:** Validates inputs? Sets `Name`?
|
||||
* [ ] **Update:** Handles `isNew` correctly? Handles `NaN`? O(1)?
|
||||
|
||||
@@ -16,7 +16,7 @@ Trend indicators help identify the direction and strength of a market trend. Mov
|
||||
| [CONV](trends/conv/Conv.md) | Convolution Indicator | Applies a custom kernel (weights) to the data window. |
|
||||
| [DEMA](trends/dema/Dema.md) | Double Exponential Moving Average | Reduces lag by placing more weight on recent data than a standard EMA. |
|
||||
| DSMA | Deviation-Scaled MA | |
|
||||
| DWMA | Double Weighted MA | |
|
||||
| [DWMA](trends/dwma/Dwma.md) | Double Weighted MA | Applies WMA smoothing twice to reduce noise further. |
|
||||
| ELLIPTIC | Elliptic (Cauer) Filter | |
|
||||
| [EMA](trends/ema/Ema.md) | Exponential Moving Average | Weighted average giving more importance to recent price data. |
|
||||
| EPMA | Endpoint MA | |
|
||||
|
||||
@@ -17,11 +17,11 @@ public class ConvValidationTests
|
||||
var sma = new Sma(period);
|
||||
var conv = new Conv(kernel);
|
||||
|
||||
var rnd = new Random(123);
|
||||
var gbm = new GBM(startPrice: 100, seed: 123);
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
double price = rnd.NextDouble() * 100;
|
||||
var tValue = new TValue(DateTime.UtcNow, price);
|
||||
var bar = gbm.Next();
|
||||
var tValue = bar.C;
|
||||
|
||||
var smaVal = sma.Update(tValue);
|
||||
var convVal = conv.Update(tValue);
|
||||
@@ -48,11 +48,11 @@ public class ConvValidationTests
|
||||
var wma = new Wma(period);
|
||||
var conv = new Conv(kernel);
|
||||
|
||||
var rnd = new Random(123);
|
||||
var gbm = new GBM(startPrice: 100, seed: 123);
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
double price = rnd.NextDouble() * 100;
|
||||
var tValue = new TValue(DateTime.UtcNow, price);
|
||||
var bar = gbm.Next();
|
||||
var tValue = bar.C;
|
||||
|
||||
var wmaVal = wma.Update(tValue);
|
||||
var convVal = conv.Update(tValue);
|
||||
@@ -105,11 +105,11 @@ public class ConvValidationTests
|
||||
var trima = new Trima(period);
|
||||
var conv = new Conv(kernel);
|
||||
|
||||
var rnd = new Random(123);
|
||||
var gbm = new GBM(startPrice: 100, seed: 123);
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
double price = rnd.NextDouble() * 100;
|
||||
var tValue = new TValue(DateTime.UtcNow, price);
|
||||
var bar = gbm.Next();
|
||||
var tValue = bar.C;
|
||||
|
||||
var trimaVal = trima.Update(tValue);
|
||||
var convVal = conv.Update(tValue);
|
||||
|
||||
@@ -79,7 +79,7 @@ public class DemaTests
|
||||
// Assert
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var val = demaObj.Update(new TValue(DateTime.Now, source[i]));
|
||||
var val = demaObj.Update(new TValue(DateTime.UtcNow, source[i]));
|
||||
Assert.Equal(val.Value, output[i], 1e-9);
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,7 @@ public class DemaTests
|
||||
// Assert
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var val = demaObj.Update(new TValue(DateTime.Now, source[i]));
|
||||
var val = demaObj.Update(new TValue(DateTime.UtcNow, source[i]));
|
||||
Assert.Equal(val.Value, output[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using Xunit;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class DwmaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void DwmaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new DwmaIndicator();
|
||||
|
||||
Assert.Equal(10, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("DWMA - Double Weighted Moving Average", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DwmaIndicator_MinHistoryDepths_EqualsTwoTimesPeriod()
|
||||
{
|
||||
var indicator = new DwmaIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(40, indicator.MinHistoryDepths);
|
||||
Assert.Equal(40, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DwmaIndicator_ShortName_IncludesPeriodAndSource()
|
||||
{
|
||||
var indicator = new DwmaIndicator { Period = 15 };
|
||||
|
||||
Assert.Contains("DWMA", indicator.ShortName);
|
||||
Assert.Contains("15", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DwmaIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new DwmaIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink);
|
||||
Assert.Contains("Dwma.Quantower.cs", indicator.SourceCodeLink);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DwmaIndicator_Initialize_CreatesInternalDwma()
|
||||
{
|
||||
var indicator = new DwmaIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DwmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new DwmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class DwmaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 10;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Dwma? ma;
|
||||
private int _warmupBarIndex = -1;
|
||||
protected LineSeries? Series;
|
||||
protected string? SourceName;
|
||||
|
||||
public int MinHistoryDepths => Period * 2; // DWMA needs roughly 2x period to warm up
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"DWMA {Period}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/dwma/Dwma.Quantower.cs";
|
||||
|
||||
public DwmaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
SourceName = Source.ToString();
|
||||
Name = "DWMA - Double Weighted Moving Average";
|
||||
Description = "Double Weighted Moving Average";
|
||||
Series = new(name: $"DWMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
ma = new Dwma(Period);
|
||||
_warmupBarIndex = -1;
|
||||
SourceName = Source.ToString();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TValue input = this.GetInputValue(args, Source);
|
||||
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
|
||||
TValue result = ma!.Update(input, isNew);
|
||||
if (_warmupBarIndex < 0 && ma!.IsHot)
|
||||
_warmupBarIndex = Count;
|
||||
Series!.SetValue(result.Value);
|
||||
Series!.SetMarker(0, Color.Transparent); //OnPaintChart draws the line, hidden here
|
||||
}
|
||||
|
||||
public override void OnPaintChart(PaintChartEventArgs args)
|
||||
{
|
||||
base.OnPaintChart(args);
|
||||
this.PaintSmoothCurve(args, Series!, _warmupBarIndex, showColdValues: ShowColdValues, tension: 0.2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class DwmaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Dwma(0));
|
||||
Assert.Throws<ArgumentException>(() => new Dwma(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ValidInput_CalculatesCorrectly()
|
||||
{
|
||||
// DWMA(3) of [1, 2, 3, 4, 5]
|
||||
// WMA(3) of [1, 2, 3, 4, 5]
|
||||
// 1: 1
|
||||
// 2: (1*1 + 2*2) / 3 = 5/3 = 1.666...
|
||||
// 3: (1*1 + 2*2 + 3*3) / 6 = 14/6 = 2.333...
|
||||
// 4: (1*2 + 2*3 + 3*4) / 6 = 20/6 = 3.333...
|
||||
// 5: (1*3 + 2*4 + 3*5) / 6 = 26/6 = 4.333...
|
||||
|
||||
// WMA(3) results: [1, 1.666, 2.333, 3.333, 4.333]
|
||||
|
||||
// DWMA(3) = WMA(3) of [1, 1.666, 2.333, 3.333, 4.333]
|
||||
// 1: 1
|
||||
// 2: (1*1 + 2*1.666) / 3 = 4.333/3 = 1.444...
|
||||
// 3: (1*1 + 2*1.666 + 3*2.333) / 6 = (1 + 3.333 + 7) / 6 = 11.333/6 = 1.888...
|
||||
|
||||
var dwma = new Dwma(3);
|
||||
|
||||
var v1 = dwma.Update(new TValue(DateTime.UtcNow, 1)).Value;
|
||||
var v2 = dwma.Update(new TValue(DateTime.UtcNow, 2)).Value;
|
||||
var v3 = dwma.Update(new TValue(DateTime.UtcNow, 3)).Value;
|
||||
|
||||
Assert.Equal(1.0, v1, 6);
|
||||
Assert.Equal(1.444444, v2, 5);
|
||||
Assert.Equal(1.888888, v3, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_CorrectsValue()
|
||||
{
|
||||
var dwma = new Dwma(3);
|
||||
|
||||
dwma.Update(new TValue(DateTime.UtcNow, 1));
|
||||
dwma.Update(new TValue(DateTime.UtcNow, 2));
|
||||
|
||||
// Update with 3, then correct to 4
|
||||
var v3 = dwma.Update(new TValue(DateTime.UtcNow, 3), isNew: true).Value;
|
||||
var v3_corrected = dwma.Update(new TValue(DateTime.UtcNow, 4), isNew: false).Value;
|
||||
|
||||
// Manual calc for sequence [1, 2, 4]
|
||||
// WMA(3):
|
||||
// 1: 1
|
||||
// 2: 1.666
|
||||
// 4: (1*1 + 2*2 + 3*4) / 6 = 17/6 = 2.8333
|
||||
|
||||
// DWMA(3) of [1, 1.666, 2.8333]
|
||||
// 3: (1*1 + 2*1.666 + 3*2.8333) / 6 = (1 + 3.333 + 8.5) / 6 = 12.833/6 = 2.1388
|
||||
|
||||
Assert.Equal(1.888888, v3, 5); // From previous test
|
||||
Assert.Equal(2.138888, v3_corrected, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var dwma = new Dwma(3);
|
||||
dwma.Update(new TValue(DateTime.UtcNow, 1));
|
||||
dwma.Update(new TValue(DateTime.UtcNow, 2));
|
||||
|
||||
dwma.Reset();
|
||||
|
||||
Assert.False(dwma.IsHot);
|
||||
var v1 = dwma.Update(new TValue(DateTime.UtcNow, 1)).Value;
|
||||
Assert.Equal(1.0, v1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticCalculate_MatchesInstance()
|
||||
{
|
||||
int period = 10;
|
||||
int count = 100;
|
||||
var source = new TSeries();
|
||||
var dwma = new Dwma(period);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), i));
|
||||
dwma.Update(source.Last);
|
||||
}
|
||||
|
||||
var staticResult = Dwma.Calculate(source, period);
|
||||
|
||||
Assert.Equal(source.Count, staticResult.Count);
|
||||
Assert.Equal(dwma.Last.Value, staticResult.Last.Value, 8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class DwmaValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Validate_Against_DoubleWma()
|
||||
{
|
||||
// DWMA should be exactly WMA(WMA(source, period), period)
|
||||
|
||||
int period = 10;
|
||||
int count = 1000;
|
||||
var source = new TSeries();
|
||||
var rnd = new Random(42);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), rnd.NextDouble() * 100));
|
||||
}
|
||||
|
||||
var dwma = new Dwma(period);
|
||||
var wma1 = new Wma(period);
|
||||
var wma2 = new Wma(period);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var val = source[i];
|
||||
|
||||
// Calculate DWMA
|
||||
var dwmaVal = dwma.Update(val);
|
||||
|
||||
// Calculate WMA(WMA) manually
|
||||
var wma1Val = wma1.Update(val);
|
||||
var wma2Val = wma2.Update(wma1Val);
|
||||
|
||||
Assert.Equal(wma2Val.Value, dwmaVal.Value, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// DWMA: Double Weighted Moving Average
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// DWMA applies a Weighted Moving Average (WMA) twice.
|
||||
/// It provides a smoother curve than a standard WMA but with slightly more lag.
|
||||
///
|
||||
/// Formula:
|
||||
/// DWMA = WMA(WMA(source, period), period)
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Dwma : ITValuePublisher
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly Wma _wma1;
|
||||
private readonly Wma _wma2;
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Current DWMA value.
|
||||
/// </summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has enough data to produce valid results.
|
||||
/// </summary>
|
||||
public bool IsHot => _wma1.IsHot && _wma2.IsHot;
|
||||
|
||||
public event Action<TValue>? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Creates DWMA with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Window size (must be > 0)</param>
|
||||
public Dwma(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_period = period;
|
||||
_wma1 = new Wma(period);
|
||||
_wma2 = new Wma(period);
|
||||
Name = $"Dwma({period})";
|
||||
}
|
||||
|
||||
public Dwma(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
source.Pub += (item) => Update(item);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
TValue wma1Result = _wma1.Update(input, isNew);
|
||||
Last = _wma2.Update(wma1Result, isNew);
|
||||
Pub?.Invoke(Last);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return new TSeries();
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
source.Times.CopyTo(tSpan);
|
||||
Calculate(source.Values, vSpan, _period);
|
||||
|
||||
// Restore state
|
||||
// We need to replay the last part to restore the internal WMAs state
|
||||
// Since DWMA is WMA(WMA), the effective lookback is roughly 2*Period
|
||||
// But to be safe and simple, we can just reset and replay the last 2*Period bars.
|
||||
|
||||
_wma1.Reset();
|
||||
_wma2.Reset();
|
||||
|
||||
int warmup = _period * 2; // Approximate warmup needed
|
||||
int startIndex = Math.Max(0, len - warmup);
|
||||
|
||||
for (int i = startIndex; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], source.Values[i]));
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public static TSeries Calculate(TSeries source, int period)
|
||||
{
|
||||
var dwma = new Dwma(period);
|
||||
return dwma.Update(source);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
throw new ArgumentException("Source and output must have the same length");
|
||||
|
||||
// We need a temporary buffer for the first WMA pass
|
||||
// Use stackalloc for small sizes, heap for large
|
||||
if (source.Length <= 1024)
|
||||
{
|
||||
Span<double> temp = stackalloc double[source.Length];
|
||||
Wma.Calculate(source, temp, period);
|
||||
Wma.Calculate(temp, output, period);
|
||||
}
|
||||
else
|
||||
{
|
||||
double[] temp = new double[source.Length];
|
||||
Wma.Calculate(source, temp, period);
|
||||
Wma.Calculate(temp, output, period);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_wma1.Reset();
|
||||
_wma2.Reset();
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# DWMA - Double Weighted Moving Average
|
||||
|
||||
DWMA is a moving average that applies the Weighted Moving Average (WMA) twice. It provides a smoother curve than a standard WMA but with slightly more lag.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
* **Double Smoothing:** Applies WMA smoothing twice to reduce noise further.
|
||||
* **Weighted:** Gives more weight to recent data points, similar to WMA.
|
||||
* **Recursive Calculation:** Uses the efficient O(1) WMA implementation.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `period` | `int` | - | The lookback period for both WMA passes. |
|
||||
|
||||
## Formula
|
||||
|
||||
$$
|
||||
DWMA_t = WMA(WMA(Price, n), n)
|
||||
$$
|
||||
|
||||
Where:
|
||||
|
||||
* $WMA$ is the Weighted Moving Average.
|
||||
* $n$ is the period.
|
||||
|
||||
## C# Implementation
|
||||
|
||||
### Standard Usage
|
||||
|
||||
```csharp
|
||||
// Create DWMA with period 14
|
||||
var dwma = new Dwma(14);
|
||||
|
||||
// Update with new value
|
||||
var result = dwma.Update(new TValue(DateTime.Now, 123.45));
|
||||
Console.WriteLine($"DWMA: {result.Value}");
|
||||
```
|
||||
|
||||
### Span API (High Performance)
|
||||
|
||||
```csharp
|
||||
// Calculate on a span of data
|
||||
ReadOnlySpan<double> input = ...;
|
||||
Span<double> output = new double[input.Length];
|
||||
|
||||
Dwma.Calculate(input, output, 14);
|
||||
```
|
||||
|
||||
### Bar Correction
|
||||
|
||||
```csharp
|
||||
// Update with a value
|
||||
dwma.Update(new TValue(time, 100), isNew: true);
|
||||
|
||||
// Correct the last value
|
||||
dwma.Update(new TValue(time, 101), isNew: false);
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
DWMA is used similarly to other moving averages to identify trends. Due to the double smoothing, it is less susceptible to whipsaws than WMA but reacts slower to price changes.
|
||||
|
||||
## References
|
||||
|
||||
* [Pine Script Implementation](https://github.com/mihakralj/pinescript/blob/main/indicators/trends_FIR/dwma.md)
|
||||
@@ -26,7 +26,7 @@ public class LsmaTests
|
||||
public void Update_SingleValue_ReturnsSameValue()
|
||||
{
|
||||
var lsma = new Lsma(14);
|
||||
var result = lsma.Update(new TValue(DateTime.Now, 100));
|
||||
var result = lsma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(100, result.Value);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public class LsmaTests
|
||||
|
||||
for (int i = 0; i < period * 2; i++)
|
||||
{
|
||||
var result = lsma.Update(new TValue(DateTime.Now, i));
|
||||
var result = lsma.Update(new TValue(DateTime.UtcNow, i));
|
||||
if (i >= period) // After warmup
|
||||
{
|
||||
Assert.Equal(i, result.Value, 1e-9);
|
||||
@@ -56,7 +56,7 @@ public class LsmaTests
|
||||
|
||||
for (int i = 0; i < period * 2; i++)
|
||||
{
|
||||
var result = lsma.Update(new TValue(DateTime.Now, value));
|
||||
var result = lsma.Update(new TValue(DateTime.UtcNow, value));
|
||||
Assert.Equal(value, result.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ public class LsmaTests
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double y = 2 * i + 1;
|
||||
var result = lsma.Update(new TValue(DateTime.Now, y));
|
||||
var result = lsma.Update(new TValue(DateTime.UtcNow, y));
|
||||
|
||||
if (i >= period)
|
||||
{
|
||||
@@ -93,20 +93,20 @@ public class LsmaTests
|
||||
// Fill buffer
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
lsma.Update(new TValue(DateTime.Now, i));
|
||||
lsma.Update(new TValue(DateTime.UtcNow, i));
|
||||
}
|
||||
|
||||
// New bar
|
||||
var result1 = lsma.Update(new TValue(DateTime.Now, 10));
|
||||
var result1 = lsma.Update(new TValue(DateTime.UtcNow, 10));
|
||||
|
||||
// Update same bar with different value
|
||||
var result2 = lsma.Update(new TValue(DateTime.Now, 20), isNew: false);
|
||||
var result2 = lsma.Update(new TValue(DateTime.UtcNow, 20), isNew: false);
|
||||
|
||||
Assert.NotEqual(result1.Value, result2.Value);
|
||||
|
||||
// Verify internal state by adding next bar
|
||||
// If state was corrupted, this would fail
|
||||
var result3 = lsma.Update(new TValue(DateTime.Now, 30));
|
||||
var result3 = lsma.Update(new TValue(DateTime.UtcNow, 30));
|
||||
Assert.True(double.IsFinite(result3.Value));
|
||||
}
|
||||
|
||||
@@ -115,9 +115,9 @@ public class LsmaTests
|
||||
{
|
||||
var lsma = new Lsma(5);
|
||||
|
||||
lsma.Update(new TValue(DateTime.Now, 1));
|
||||
lsma.Update(new TValue(DateTime.Now, 2));
|
||||
var result = lsma.Update(new TValue(DateTime.Now, double.NaN));
|
||||
lsma.Update(new TValue(DateTime.UtcNow, 1));
|
||||
lsma.Update(new TValue(DateTime.UtcNow, 2));
|
||||
var result = lsma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
// Input sequence becomes: 1, 2, 2 (NaN replaced by last valid 2)
|
||||
// Regression on (2,1), (1,2), (0,2)
|
||||
@@ -131,11 +131,12 @@ public class LsmaTests
|
||||
int period = 10;
|
||||
int count = 100;
|
||||
var source = new TSeries();
|
||||
var rnd = new Random(42);
|
||||
var gbm = new GBM(startPrice: 100, seed: 42);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.Now.AddMinutes(i), rnd.NextDouble() * 100));
|
||||
var bar = gbm.Next();
|
||||
source.Add(bar.C);
|
||||
}
|
||||
|
||||
var lsma = new Lsma(period);
|
||||
@@ -156,11 +157,12 @@ public class LsmaTests
|
||||
int count = 100;
|
||||
var values = new double[count];
|
||||
var output = new double[count];
|
||||
var rnd = new Random(42);
|
||||
var gbm = new GBM(startPrice: 100, seed: 42);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
values[i] = rnd.NextDouble() * 100;
|
||||
var bar = gbm.Next();
|
||||
values[i] = bar.Close;
|
||||
}
|
||||
|
||||
Lsma.Calculate(values, output, period);
|
||||
@@ -168,7 +170,7 @@ public class LsmaTests
|
||||
var lsma = new Lsma(period);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var result = lsma.Update(new TValue(DateTime.Now, values[i]));
|
||||
var result = lsma.Update(new TValue(DateTime.UtcNow, values[i]));
|
||||
Assert.Equal(result.Value, output[i], 1e-9);
|
||||
}
|
||||
}
|
||||
@@ -179,7 +181,7 @@ public class LsmaTests
|
||||
var lsma = new Lsma(5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
lsma.Update(new TValue(DateTime.Now, i));
|
||||
lsma.Update(new TValue(DateTime.UtcNow, i));
|
||||
}
|
||||
|
||||
Assert.True(lsma.IsHot);
|
||||
@@ -190,7 +192,7 @@ public class LsmaTests
|
||||
Assert.Equal(0, lsma.Last.Value);
|
||||
|
||||
// Should behave like new instance
|
||||
var result = lsma.Update(new TValue(DateTime.Now, 100));
|
||||
var result = lsma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(100, result.Value);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user