Add RMA indicator implementation and related tests; update existing indicators to return empty array for zero count

This commit is contained in:
Miha Kralj
2025-12-10 19:24:55 -05:00
parent 8df3480d1f
commit 66b1fc3dca
16 changed files with 854 additions and 8 deletions
+123
View File
@@ -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`.
+1 -1
View File
@@ -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 | |
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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);
+169
View File
@@ -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);
}
}
+65
View File
@@ -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);
}
}
+205
View File
@@ -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));
}
}
+56
View File
@@ -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);
}
}
}
}
+116
View File
@@ -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();
}
}
+111
View File
@@ -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.
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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);
+1
View File
@@ -7,6 +7,7 @@
<OutputPath>bin\$(Configuration)\</OutputPath>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<IsPackable>false</IsPackable>
<SonarQubeExclude>true</SonarQubeExclude>
</PropertyGroup>
<ItemGroup>