mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-05 20:47:43 +00:00
Add RMA indicator implementation and related tests; update existing indicators to return empty array for zero count
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
# AGENTS.md - QuanTAlib Protocol
|
||||
|
||||
> **To all AI Agents:** This file defines the laws, physics, and protocols of the QuanTAlib repository. Read this before writing a single line of code. Failure to adhere to these standards will result in rejected code.
|
||||
|
||||
## 1. Identity & Mission
|
||||
|
||||
**QuanTAlib** is a high-performance, zero-allocation C# library for quantitative technical analysis.
|
||||
|
||||
* **Target**: Quantower and custom C# trading engines.
|
||||
* **Core Philosophy**: Speed, Correctness, and Memory Efficiency.
|
||||
* **Key Constraint**: Hot paths must be allocation-free (GC pressure is the enemy).
|
||||
|
||||
## 2. Architecture & "Physics"
|
||||
|
||||
### Memory Model: Structure of Arrays (SoA)
|
||||
|
||||
We do not store objects in lists. We store primitive arrays.
|
||||
|
||||
* **TSeries**: Internally uses `List<long> _t` (timestamps) and `List<double> _v` (values).
|
||||
* **Access**: Expose data via `ReadOnlySpan<double>` for SIMD operations.
|
||||
|
||||
### Core Types
|
||||
|
||||
* `TValue`: Struct (16 bytes). `DateTime Time`, `double Value`.
|
||||
* `TBar`: Struct (48 bytes). `DateTime Time`, `double Open, High, Low, Close, Volume`.
|
||||
* `TSeries`: The primary data structure for time series.
|
||||
* `ITValuePublisher`: The interface for reactive data flow.
|
||||
|
||||
### Performance Rules
|
||||
|
||||
1. **Zero Allocation**: The `Update` method MUST NOT allocate memory on the heap. Use `stackalloc` or pre-allocated buffers.
|
||||
2. **O(1) Complexity**: Streaming updates must be constant time. Use circular buffers (`RingBuffer`) or running sums.
|
||||
3. **SIMD**: Batch operations (`Calculate`) should use `System.Runtime.Intrinsics` (AVX2) where possible.
|
||||
4. **Inlining**: Use `[MethodImpl(MethodImplOptions.AggressiveInlining)]` on hot methods.
|
||||
5. **Locals**: Use `[SkipLocalsInit]` to avoid zero-init costs in tight loops.
|
||||
|
||||
## 3. Indicator Implementation Standards
|
||||
|
||||
Every indicator must follow the **Good Indicator Guidelines** strictly.
|
||||
|
||||
### File Structure
|
||||
|
||||
Directory: `lib/[category]/[name]/` (e.g., `lib/trends/sma/`)
|
||||
|
||||
| File | Naming | Purpose |
|
||||
|------|--------|---------|
|
||||
| **Source** | `[Name].cs` | Main logic. `public sealed class`. |
|
||||
| **Tests** | `[Name].Tests.cs` | xUnit tests (correctness, edge cases). |
|
||||
| **Validation** | `[Name].Validation.Tests.cs` | Compare against TA-Lib, Skender, etc. |
|
||||
| **Docs** | `[Name].md` | User documentation with formulas. |
|
||||
| **Adapter** | `[Name].Quantower.cs` | Quantower platform integration. |
|
||||
| **Adapter Tests** | `[Name].Quantower.Tests.cs` | Tests for the adapter. |
|
||||
|
||||
### The `Update` Method Contract
|
||||
|
||||
The `Update` method is the heart of the indicator.
|
||||
|
||||
```csharp
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
```
|
||||
|
||||
* **`isNew = true`**: A new bar has arrived. Save current state to history (or `_p_` variables), then calculate.
|
||||
* **`isNew = false`**: The current bar is updating (tick data). Restore state from history (or `_p_` variables), then recalculate.
|
||||
* **NaN Handling**: If input is `NaN` or `Infinity`, use the last valid value. Never propagate `NaN`.
|
||||
|
||||
### State Management
|
||||
|
||||
* Use `RingBuffer` for sliding windows.
|
||||
* Maintain `_state` and `_p_state` (previous state) variables to support `isNew=false` rollbacks.
|
||||
* **Resync**: Periodically recalculate running sums to prevent floating-point drift.
|
||||
|
||||
### Dual API Requirement
|
||||
|
||||
1. **Stateful (Streaming)**: `Update(TValue)` for live data.
|
||||
2. **Stateless (Vector)**: `static void Calculate(ReadOnlySpan<double> src, Span<double> dst)` for batch history.
|
||||
|
||||
## 4. Testing Protocol
|
||||
|
||||
### Unit Tests (`[Name].Tests.cs`)
|
||||
|
||||
* Use `GBM` (Geometric Brownian Motion) for data generation.
|
||||
* Test `isNew=true` vs `isNew=false` consistency.
|
||||
* Test `Reset()` and `IsHot` (warmup).
|
||||
* Test edge cases: `NaN` inputs, empty series, period=1.
|
||||
|
||||
### Validation Tests (`[Name].Validation.Tests.cs`)
|
||||
|
||||
* **Mandatory**: You MUST validate against at least one external authority (TA-Lib, Skender, Tulip, Python libs).
|
||||
* **Tolerance**: Typically `1e-6` to `1e-9`.
|
||||
|
||||
## 5. Documentation Standards
|
||||
|
||||
* **Format**: Markdown.
|
||||
* **Content**: Title, Description, Parameters, Formula (LaTeX), C# Usage Examples.
|
||||
* **Index**: Add the new indicator to the category index (e.g., `lib/trends/_index.md`).
|
||||
|
||||
## 6. Development Checklist
|
||||
|
||||
When creating a new indicator, you are **DONE** only when:
|
||||
|
||||
* [ ] Source algorithm is verified.
|
||||
* [ ] All 6 required files exist.
|
||||
* [ ] `Update` handles `isNew` and `NaN` correctly.
|
||||
* [ ] No heap allocations in `Update`.
|
||||
* [ ] Static `Calculate(Span)` is implemented.
|
||||
* [ ] Unit tests pass (including edge cases).
|
||||
* [ ] Validation tests pass against external libs.
|
||||
* [ ] Documentation is complete and linked in `_index.md`.
|
||||
* [ ] CodeRabbit review issues are resolved.
|
||||
|
||||
## 7. Forbidden Actions
|
||||
|
||||
* **DO NOT** use LINQ in hot paths (`Update` or `Calculate`).
|
||||
* **DO NOT** use `new` inside `Update`.
|
||||
* **DO NOT** change `Directory.Build.props` without explicit instruction.
|
||||
* **DO NOT** remove `[SkipLocalsInit]` or `[MethodImpl]` attributes.
|
||||
* **DO NOT** ignore `NaN` inputs; handle them safely.
|
||||
|
||||
## 8. Context & Resources
|
||||
|
||||
* **Time**: Use `DateTime.UtcNow`.
|
||||
* **Math**: Use `System.Math` or `System.Numerics`.
|
||||
* **Root Namespace**: `QuanTAlib`.
|
||||
@@ -47,7 +47,7 @@ Trend indicators help identify the direction and strength of a market trend. Mov
|
||||
| QEMA | Quadruple Exponential MA | |
|
||||
| REMA | Regularized Exponential MA | |
|
||||
| RGMA | Recursive Gaussian MA | |
|
||||
| RMA | wildeR MA (SMMA, MMA) | |
|
||||
| [RMA](trends/rma/Rma.md) | wildeR MA (SMMA, MMA) | Exponential moving average with alpha = 1/N. |
|
||||
| SGF | Savitzky-Golay Filter | |
|
||||
| SGMA | Savitzky-Golay MA | |
|
||||
| SINEMA | Sine-weighted MA | |
|
||||
|
||||
@@ -117,7 +117,7 @@ public sealed class Conv : ITValuePublisher
|
||||
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return new TSeries();
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
int len = source.Count;
|
||||
List<long> t = new(len);
|
||||
|
||||
@@ -123,7 +123,7 @@ public sealed class Dema : ITValuePublisher
|
||||
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return new TSeries();
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
int len = source.Count;
|
||||
List<long> t = new(len);
|
||||
|
||||
@@ -69,7 +69,7 @@ public sealed class Dwma : ITValuePublisher
|
||||
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return new TSeries();
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
|
||||
@@ -224,7 +224,7 @@ public sealed class Mama : ITValuePublisher
|
||||
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return new TSeries();
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
int len = source.Count;
|
||||
var v = new List<double>(len);
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
using Xunit;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RmaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void RmaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new RmaIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("RMA - Running Moving Average", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmaIndicator_MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var indicator = new RmaIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(20, indicator.MinHistoryDepths);
|
||||
Assert.Equal(20, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmaIndicator_ShortName_IncludesPeriodAndSource()
|
||||
{
|
||||
var indicator = new RmaIndicator { Period = 15 };
|
||||
|
||||
Assert.Contains("RMA", indicator.ShortName);
|
||||
Assert.Contains("15", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmaIndicator_Initialize_CreatesInternalRma()
|
||||
{
|
||||
var indicator = new RmaIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmaIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
// Process first update
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
// Line series should have values
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new RmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process historical bar first
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Update with new tick (same bar data - simulates intrabar update)
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Both values should be finite
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmaIndicator_MultipleUpdates_ProducesCorrectRmaSequence()
|
||||
{
|
||||
var indicator = new RmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 104, 103, 105, 107, 106 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
|
||||
// RMA should be smoothing the values
|
||||
// Last RMA value should be between first and last close
|
||||
double lastRma = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(lastRma >= 100 && lastRma <= 110);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmaIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new RmaIndicator { Period = 3, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmaIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new RmaIndicator { Period = 5 };
|
||||
Assert.Equal(5, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(20, indicator.MinHistoryDepths);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class RmaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Rma? ma;
|
||||
protected LineSeries? Series;
|
||||
protected string? SourceName;
|
||||
private int _warmupBarIndex = -1;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"RMA {Period}:{SourceName}";
|
||||
|
||||
public RmaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
SourceName = Source.ToString();
|
||||
Name = "RMA - Running Moving Average";
|
||||
Description = "Running Moving Average (Wilder's Smoothing)";
|
||||
Series = new(name: $"RMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
ma = new Rma(Period);
|
||||
SourceName = Source.ToString();
|
||||
_warmupBarIndex = -1; // Reset warmup tracking when period changes
|
||||
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);
|
||||
Series!.SetValue(result.Value);
|
||||
Series!.SetMarker(0, Color.Transparent); //OnPaintChart draws the line, hidden here
|
||||
|
||||
// Track when IsHot becomes true for the first time
|
||||
if (_warmupBarIndex < 0 && ma!.IsHot)
|
||||
_warmupBarIndex = Count;
|
||||
}
|
||||
|
||||
public override void OnPaintChart(PaintChartEventArgs args)
|
||||
{
|
||||
base.OnPaintChart(args);
|
||||
int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count;
|
||||
this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes
|
||||
public class RmaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Rma_Constructor_Period_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Rma(0));
|
||||
Assert.Throws<ArgumentException>(() => new Rma(-1));
|
||||
|
||||
var rma = new Rma(10);
|
||||
Assert.NotNull(rma);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rma_Calc_ReturnsValue()
|
||||
{
|
||||
var rma = new Rma(10);
|
||||
|
||||
Assert.Equal(0, rma.Last.Value);
|
||||
|
||||
TValue result = rma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.True(result.Value > 0);
|
||||
Assert.Equal(result.Value, rma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rma_Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var rma = new Rma(10);
|
||||
|
||||
rma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
double value1 = rma.Last.Value;
|
||||
|
||||
rma.Update(new TValue(DateTime.UtcNow, 105), isNew: true);
|
||||
double value2 = rma.Last.Value;
|
||||
|
||||
// Values should change with new bars
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rma_Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var rma = new Rma(10);
|
||||
|
||||
rma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
rma.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
|
||||
double beforeUpdate = rma.Last.Value;
|
||||
|
||||
rma.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
|
||||
double afterUpdate = rma.Last.Value;
|
||||
|
||||
// Update should change the value
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rma_Reset_ClearsState()
|
||||
{
|
||||
var rma = new Rma(10);
|
||||
|
||||
rma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
rma.Update(new TValue(DateTime.UtcNow, 105));
|
||||
double valueBefore = rma.Last.Value;
|
||||
|
||||
rma.Reset();
|
||||
|
||||
Assert.Equal(0, rma.Last.Value);
|
||||
|
||||
// After reset, should accept new values
|
||||
rma.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.NotEqual(0, rma.Last.Value);
|
||||
Assert.NotEqual(valueBefore, rma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rma_IsHot_BecomesTrueAt95PercentCoverage()
|
||||
{
|
||||
var rma = new Rma(10);
|
||||
|
||||
// Initially IsHot should be false
|
||||
Assert.False(rma.IsHot);
|
||||
|
||||
// IsHot triggers at 95% coverage (E <= 0.05)
|
||||
// E = (1 - alpha)^N where alpha = 1 / period
|
||||
// For period 10: alpha = 0.1, (1-alpha) = 0.9
|
||||
// N = ln(0.05) / ln(0.9) ≈ 28.4, so ~29 bars
|
||||
|
||||
int steps = 0;
|
||||
while (!rma.IsHot && steps < 1000)
|
||||
{
|
||||
rma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
steps++;
|
||||
}
|
||||
|
||||
Assert.True(rma.IsHot);
|
||||
Assert.True(steps > 0);
|
||||
// For period 10, should become hot around 29 bars
|
||||
Assert.InRange(steps, 28, 30);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rma_EquivalentToEmaWithAlpha()
|
||||
{
|
||||
int period = 10;
|
||||
double alpha = 1.0 / period;
|
||||
|
||||
var rma = new Rma(period);
|
||||
var ema = new Ema(alpha);
|
||||
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
var rmaVal = rma.Update(new TValue(bar.Time, bar.Close));
|
||||
var emaVal = ema.Update(new TValue(bar.Time, bar.Close));
|
||||
|
||||
Assert.Equal(emaVal.Value, rmaVal.Value, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rma_BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var rmaIterative = new Rma(10);
|
||||
var rmaBatch = new Rma(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Generate data
|
||||
var series = new TSeries();
|
||||
var inputList = new List<TValue>();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
inputList.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var item in inputList)
|
||||
{
|
||||
iterativeResults.Add(rmaIterative.Update(item));
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = rmaBatch.Update(series);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(series.Count, iterativeResults.Count);
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < inputList.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rma_SpanCalc_MatchesTSeriesCalc()
|
||||
{
|
||||
var series = new TSeries();
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
source[i] = bar.Close;
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
// Calculate with TSeries API
|
||||
var tseriesResult = Rma.Calculate(series, 10);
|
||||
|
||||
// Calculate with Span API
|
||||
Rma.Calculate(source.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
// Compare results
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(tseriesResult[i].Value, output[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rma_NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var rma = new Rma(10);
|
||||
|
||||
// Feed some valid values
|
||||
rma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
rma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
// Feed NaN - should use last valid value (110)
|
||||
var resultAfterNaN = rma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
// Result should be finite (not NaN)
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Skender.Stock.Indicators;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RmaValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Rma_Matches_Skender_Smma()
|
||||
{
|
||||
// Arrange
|
||||
int period = 14;
|
||||
int length = 1000;
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
var bars = gbm.Fetch(length, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// QuanTAlib RMA
|
||||
var rma = new Rma(period);
|
||||
var quantalibResults = new TSeries();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
quantalibResults.Add(rma.Update(new TValue(bar.Time, bar.Close)));
|
||||
}
|
||||
|
||||
// Skender SMMA
|
||||
var quotes = bars.Select(b => new Quote
|
||||
{
|
||||
Date = new DateTime(b.Time, DateTimeKind.Utc),
|
||||
Open = (decimal)b.Open,
|
||||
High = (decimal)b.High,
|
||||
Low = (decimal)b.Low,
|
||||
Close = (decimal)b.Close,
|
||||
Volume = (decimal)b.Volume
|
||||
}).ToList();
|
||||
|
||||
var skenderResults = quotes.GetSmma(period).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(quantalibResults.Count, skenderResults.Count);
|
||||
|
||||
// Skip warmup period for comparison
|
||||
// Skender uses SMA initialization, QuanTAlib uses zero-lag compensator
|
||||
// They should converge after some periods
|
||||
int skip = period * 20;
|
||||
|
||||
for (int i = skip; i < length; i++)
|
||||
{
|
||||
double qValue = quantalibResults[i].Value;
|
||||
double? sValue = skenderResults[i].Smma;
|
||||
|
||||
if (sValue.HasValue)
|
||||
{
|
||||
Assert.Equal(sValue.Value, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// RMA: Running Moving Average (also known as Wilder's Moving Average or SMMA)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// RMA is an Exponential Moving Average (EMA) with a different smoothing factor.
|
||||
/// While EMA uses alpha = 2 / (period + 1), RMA uses alpha = 1 / period.
|
||||
///
|
||||
/// Calculation:
|
||||
/// alpha = 1 / period
|
||||
/// RMA_new = RMA_old + alpha * (newest - RMA_old)
|
||||
///
|
||||
/// This implementation wraps the EMA implementation to ensure identical behavior and performance,
|
||||
/// utilizing the same O(1) update complexity and zero-allocation architecture.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Rma : ITValuePublisher
|
||||
{
|
||||
private readonly Ema _ema;
|
||||
private readonly int _period;
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name => $"Rma({_period})";
|
||||
|
||||
public event Action<TValue>? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Creates RMA with specified period.
|
||||
/// Alpha = 1 / period
|
||||
/// </summary>
|
||||
/// <param name="period">Period for RMA calculation (must be > 0)</param>
|
||||
public Rma(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_period = period;
|
||||
_ema = new Ema(1.0 / period);
|
||||
_ema.Pub += (item) => Pub?.Invoke(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates RMA with specified source and period.
|
||||
/// Subscribes to source.Pub event.
|
||||
/// </summary>
|
||||
/// <param name="source">Source to subscribe to</param>
|
||||
/// <param name="period">Period for RMA calculation</param>
|
||||
public Rma(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
source.Pub += (item) => Update(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current RMA value.
|
||||
/// </summary>
|
||||
public TValue Last => _ema.Last;
|
||||
|
||||
/// <summary>
|
||||
/// True if the RMA has warmed up and is providing valid results.
|
||||
/// </summary>
|
||||
public bool IsHot => _ema.IsHot;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
return _ema.Update(input, isNew);
|
||||
}
|
||||
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
return _ema.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates RMA for the entire series using a new instance.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <param name="period">RMA period</param>
|
||||
/// <returns>RMA series</returns>
|
||||
public static TSeries Calculate(TSeries source, int period)
|
||||
{
|
||||
var rma = new Rma(period);
|
||||
return rma.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates RMA in-place using period, writing results to pre-allocated output span.
|
||||
/// Zero-allocation method for maximum performance.
|
||||
/// Alpha = 1 / period
|
||||
/// </summary>
|
||||
/// <param name="source">Input values</param>
|
||||
/// <param name="output">Output span (must be same length as source)</param>
|
||||
/// <param name="period">RMA period (must be > 0)</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
double alpha = 1.0 / period;
|
||||
Ema.Calculate(source, output, alpha);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the RMA state.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_ema.Reset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
# RMA: Wilder's Moving Average
|
||||
|
||||
[Pine Script Implementation of RMA](https://github.com/mihakralj/pinescript/blob/main/indicators/trends_IIR/rma.pine)
|
||||
|
||||
## Overview and Purpose
|
||||
|
||||
Wilder's Moving Average (RMA), also known as the Smoothed Moving Average (SMMA), is a specialized technical indicator designed to provide superior noise reduction while maintaining sensitivity to meaningful price changes. Developed by J. Welles Wilder Jr. and introduced in his influential 1978 book "New Concepts in Technical Trading Systems," RMA was specifically created to power Wilder's revolutionary technical indicators like RSI, ATR, and DMI/ADX.
|
||||
|
||||
RMA achieves its distinctive smoothing characteristics by using a specific smoothing factor of 1/period, positioning it as an intermediate option between the simple moving average (SMA) and the standard exponential moving average (EMA). This unique approach provides the consistent, well-behaved smoothing necessary for Wilder's indicators to function properly.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- **Specialized smoothing:** Uses a fixed 1/period smoothing factor that creates more stable output than standard EMA
|
||||
- **Noise reduction:** Superior filtering of market noise compared to EMA while maintaining better responsiveness than SMA
|
||||
- **Indicator foundation:** Forms the mathematical basis for Wilder's suite of technical indicators (RSI, ATR, ADX)
|
||||
- **Balanced response:** Provides an optimal middle ground between the responsiveness of EMA and the stability of SMA
|
||||
|
||||
RMA achieves its unique characteristics by applying a smoothing factor ($\alpha = 1/N$) that is consistently lower than the standard EMA formula ($\alpha = 2/(N+1)$). This makes RMA approximately twice as slow to react compared to a standard EMA of the same period length, creating a smoother line that better filters out market noise.
|
||||
|
||||
## Common Settings and Parameters
|
||||
|
||||
| Parameter | Default | Function | When to Adjust |
|
||||
|-----------|---------|----------|---------------|
|
||||
| Length | 14 | Controls the amount of smoothing | Wilder's original indicators used 14; increase for more smoothing, decrease for more responsiveness |
|
||||
| Source | Close | Data point used for calculation | Change to High/Low for volatility measures or HL2/HLC3 for balanced price representation |
|
||||
| Alpha override | auto | Direct control of smoothing factor | Set manually to fine-tune behavior beyond standard period settings |
|
||||
|
||||
**Pro Tip:** When replacing RMA in Wilder's original indicators with other moving averages, remember that an EMA with twice the period length (e.g., EMA(28)) will approximate the smoothing behavior of RMA(14).
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
|
||||
**Simplified explanation:**
|
||||
RMA works by taking a small portion (1/period) of the current price and adding it to a large portion ((period-1)/period) of the previous RMA value. This creates a very smooth moving average that reduces market noise while still adapting to price changes over time.
|
||||
|
||||
**Technical formula:**
|
||||
$$
|
||||
RMA_t = \alpha \cdot P_t + (1 - \alpha) \cdot RMA_{t-1}
|
||||
$$
|
||||
|
||||
Where:
|
||||
|
||||
- $RMA_t$ is the RMA value at time $t$
|
||||
- $P_t$ is the price at time $t$
|
||||
- $N$ is the period
|
||||
- $\alpha = 1/N$
|
||||
|
||||
> 🔍 **Technical Note:** Advanced implementations use mathematical compensation methods that correct initialization bias, providing accurate values from the first bar without waiting for a "warm-up" period. This compensation is calculated as: $RMA_{corrected} = RMA_{raw} / (1 - compensation)$, where compensation decays by $(1-\alpha)$ on each bar.
|
||||
|
||||
## C# Implementation
|
||||
|
||||
### Standard Usage
|
||||
|
||||
```csharp
|
||||
// Create RMA with period 14
|
||||
var rma = new Rma(14);
|
||||
|
||||
// Update with new values
|
||||
var result = rma.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Console.WriteLine($"RMA: {result.Value}");
|
||||
```
|
||||
|
||||
### Span API (High Performance)
|
||||
|
||||
```csharp
|
||||
// Calculate RMA on a span of data
|
||||
double[] source = ...;
|
||||
double[] output = new double[source.Length];
|
||||
|
||||
// Zero-allocation calculation
|
||||
Rma.Calculate(source, output, 14);
|
||||
```
|
||||
|
||||
### Event-Driven
|
||||
|
||||
```csharp
|
||||
// Subscribe to a feed
|
||||
var feed = new CsvFeed("data.csv");
|
||||
var rma = new Rma(feed, 14);
|
||||
|
||||
rma.Pub += (item) => Console.WriteLine($"RMA: {item.Value}");
|
||||
```
|
||||
|
||||
## Interpretation Details
|
||||
|
||||
RMA provides several key benefits for technical analysis:
|
||||
|
||||
- Creates smoother trend lines compared to EMA, making trend direction easier to identify
|
||||
- Reduces whipsaws and false signals in indicator calculations
|
||||
- Maintains consistency across all of Wilder's indicators, enabling proper interpretation
|
||||
- Functions as an effective dynamic support/resistance level in trending markets
|
||||
- Provides stable baselines for measuring price momentum and volatility
|
||||
|
||||
RMA is primarily used as a smoothing component in other indicators rather than a standalone trend indicator.
|
||||
|
||||
- **RSI:** Uses RMA to smooth gains and losses.
|
||||
- **ATR:** Uses RMA to smooth true range.
|
||||
- **ADX:** Uses RMA to smooth directional movement.
|
||||
|
||||
## Limitations and Considerations
|
||||
|
||||
- **Market conditions:** Slower response makes it less suitable for fast-moving markets or short timeframes
|
||||
- **Lag factor:** Exhibits more lag than standard EMA due to the smaller smoothing factor (approximately twice as much)
|
||||
- **Specialized use:** Primarily designed for Wilder's indicators rather than as a general-purpose moving average
|
||||
- **Parameter inflexibility:** Using the fixed 1/period smoothing factor reduces tuning options
|
||||
- **Complementary tools:** Best used with faster indicators or price action analysis to compensate for the lag
|
||||
|
||||
## References
|
||||
|
||||
1. Wilder, J.W. (1978). *New Concepts in Technical Trading Systems*. Trend Research.
|
||||
2. Murphy, J.J. (1999). *Technical Analysis of the Financial Markets*. New York Institute of Finance.
|
||||
3. Kaufman, P.J. (2013). *Trading Systems and Methods*, 5th Edition. Wiley Trading.
|
||||
@@ -132,7 +132,7 @@ public sealed class Tema : ITValuePublisher
|
||||
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return new TSeries();
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
int len = source.Count;
|
||||
List<long> t = new(len);
|
||||
|
||||
@@ -141,7 +141,7 @@ public sealed class Trima : ITValuePublisher
|
||||
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return new TSeries();
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
int len = source.Count;
|
||||
List<long> t = new(len);
|
||||
|
||||
@@ -141,7 +141,7 @@ public sealed class Wma : ITValuePublisher
|
||||
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return new TSeries();
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
int len = source.Count;
|
||||
List<long> t = new(len);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<IsPackable>false</IsPackable>
|
||||
<SonarQubeExclude>true</SonarQubeExclude>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user