Remove multiple Pine Script indicators: SSFDSP, STARCHANNEL, STBANDS, STC, UBANDS, UCHANNEL, VWAPBANDS, and VWAPSD. These indicators were deleted to streamline the library and remove unused or redundant code.

This commit is contained in:
Miha Kralj
2026-02-20 18:44:56 -08:00
parent 3dd05f23e4
commit cbeefc9d64
283 changed files with 23963 additions and 3838 deletions
+107
View File
@@ -0,0 +1,107 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public sealed class ErIndicatorTests
{
[Fact]
public void ErIndicator_Constructor_SetsDefaults()
{
var indicator = new ErIndicator();
Assert.Equal(10, indicator.Period);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("ER - Kaufman Efficiency Ratio", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void ErIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new ErIndicator { Period = 10 };
Assert.Equal(0, ErIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void ErIndicator_ShortName_IncludesParameters()
{
var indicator = new ErIndicator { Period = 20 };
indicator.Initialize();
Assert.Contains("ER", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void ErIndicator_SourceCodeLink_IsValid()
{
var indicator = new ErIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Er.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void ErIndicator_Initialize_CreatesInternalEr()
{
var indicator = new ErIndicator { Period = 10 };
indicator.Initialize();
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void ErIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new ErIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
double value = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(value));
}
[Fact]
public void ErIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new ErIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void ErIndicator_Parameters_CanBeChanged()
{
var indicator = new ErIndicator { Period = 20 };
indicator.Initialize();
Assert.Equal(20, indicator.Period);
}
}
+63
View File
@@ -0,0 +1,63 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class ErIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 500, 1, 0)]
public int Period { get; set; } = 10;
[IndicatorExtensions.DataSourceInput(sortIndex: 2)]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Er _er = null!;
private readonly LineSeries _erLine;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"ER ({Period})";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/er/Er.Quantower.cs";
public ErIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "ER - Kaufman Efficiency Ratio";
Description = "Measures signal-to-noise ratio: 1 = trending, 0 = choppy";
_erLine = new LineSeries("ER", Color.Yellow, 2, LineStyle.Solid);
AddLineSeries(_erLine);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_er = new Er(Period);
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
var priceSelector = Source.GetPriceSelector();
var item = HistoricalData[0, SeekOriginHistory.End];
double price = priceSelector(item);
TValue input = new(item.TimeLeft, price);
TValue result = _er.Update(input, args.IsNewBar());
if (!_er.IsHot && !ShowColdValues)
{
return;
}
_erLine.SetValue(result.Value);
}
}
+342
View File
@@ -0,0 +1,342 @@
using Xunit;
namespace QuanTAlib.Tests;
public sealed class ErTests
{
private const double Tolerance = 1e-9;
// ───── A) Constructor validation ─────
[Fact]
public void Constructor_DefaultPeriod_IsValid()
{
var er = new Er();
Assert.Equal(10, er.Period);
Assert.Equal("Er(10)", er.Name);
}
[Fact]
public void Constructor_InvalidPeriod_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Er(period: 0));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_NegativePeriod_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Er(period: -5));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_CustomPeriod_SetsCorrectly()
{
var er = new Er(period: 20);
Assert.Equal(20, er.Period);
Assert.Equal("Er(20)", er.Name);
}
// ───── B) Basic calculation ─────
[Fact]
public void Update_ReturnsTValue()
{
var er = new Er(period: 5);
var result = er.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.IsType<TValue>(result);
}
[Fact]
public void Update_Last_IsAccessible()
{
var er = new Er(period: 5);
er.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.True(double.IsFinite(er.Last.Value));
}
[Fact]
public void Update_TrendingPrices_HighER()
{
var er = new Er(period: 10);
for (int i = 0; i < 20; i++)
{
er.Update(new TValue(DateTime.UtcNow, 100.0 + i * 2));
}
Assert.True(er.Last.Value > 0.8, "Strongly trending prices should produce high ER");
}
[Fact]
public void Update_ChoppyPrices_LowER()
{
var er = new Er(period: 10);
for (int i = 0; i < 30; i++)
{
double price = 100.0 + (i % 2 == 0 ? 5.0 : -5.0);
er.Update(new TValue(DateTime.UtcNow, price));
}
Assert.True(er.Last.Value < 0.3, "Choppy prices should produce low ER");
}
[Fact]
public void Update_Output_ClampedTo01()
{
var er = new Er(period: 5);
for (int i = 0; i < 20; i++)
{
var result = er.Update(new TValue(DateTime.UtcNow, 100.0 + i));
Assert.InRange(result.Value, 0.0, 1.0);
}
}
// ───── C) State + bar correction ─────
[Fact]
public void Update_IsNew_False_RollsBack()
{
var er = new Er(period: 5);
for (int i = 0; i < 12; i++)
{
er.Update(new TValue(DateTime.UtcNow, 100.0 + i), isNew: true);
}
er.Update(new TValue(DateTime.UtcNow, 105.0), isNew: false);
var corrected = er.Last;
er.Update(new TValue(DateTime.UtcNow, 105.0), isNew: false);
var corrected2 = er.Last;
Assert.Equal(corrected.Value, corrected2.Value, Tolerance);
}
[Fact]
public void Update_IterativeCorrections_Restore()
{
var er = new Er(period: 5);
double[] data = new double[15];
for (int i = 0; i < data.Length; i++)
{
data[i] = 100 + i * 2;
}
for (int i = 0; i < data.Length; i++)
{
er.Update(new TValue(DateTime.UtcNow, data[i]), isNew: true);
}
var baseline = er.Last.Value;
er.Update(new TValue(DateTime.UtcNow, 999.0), isNew: false);
er.Update(new TValue(DateTime.UtcNow, 888.0), isNew: false);
er.Update(new TValue(DateTime.UtcNow, data[^1]), isNew: false);
Assert.Equal(baseline, er.Last.Value, Tolerance);
}
[Fact]
public void Reset_ClearsState()
{
var er = new Er(period: 5);
for (int i = 0; i < 10; i++)
{
er.Update(new TValue(DateTime.UtcNow, 100.0 + i));
}
er.Reset();
Assert.False(er.IsHot);
Assert.Equal(0.0, er.Last.Value);
}
// ───── D) Warmup/convergence ─────
[Fact]
public void IsHot_FlipsWhenBufferFull()
{
int period = 10;
var er = new Er(period);
for (int i = 0; i < period; i++)
{
er.Update(new TValue(DateTime.UtcNow, 100.0 + i));
Assert.False(er.IsHot);
}
er.Update(new TValue(DateTime.UtcNow, 120.0));
Assert.True(er.IsHot);
}
[Fact]
public void WarmupPeriod_MatchesPeriodPlusOne()
{
var er = new Er(period: 14);
Assert.Equal(15, er.WarmupPeriod);
}
// ───── E) Robustness ─────
[Fact]
public void Update_NaN_UsesLastValid()
{
var er = new Er(period: 5);
for (int i = 0; i < 10; i++)
{
er.Update(new TValue(DateTime.UtcNow, 100.0 + i));
}
er.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(er.Last.Value));
}
[Fact]
public void Update_Infinity_UsesLastValid()
{
var er = new Er(period: 5);
for (int i = 0; i < 10; i++)
{
er.Update(new TValue(DateTime.UtcNow, 100.0 + i));
}
er.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(er.Last.Value));
}
[Fact]
public void Update_BatchNaN_RemainsFinite()
{
var er = new Er(period: 5);
for (int i = 0; i < 3; i++)
{
er.Update(new TValue(DateTime.UtcNow, double.NaN));
}
Assert.True(double.IsFinite(er.Last.Value));
}
// ───── F) Consistency (4 modes match) ─────
[Fact]
public void AllModes_ProduceSameResults()
{
int period = 10;
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
// 1. Streaming
var streaming = new Er(period);
var streamResults = new double[source.Count];
for (int i = 0; i < source.Count; i++)
{
streamResults[i] = streaming.Update(source[i]).Value;
}
// 2. Batch TSeries
TSeries batchSeries = Er.Batch(source, period);
// 3. Batch Span
var spanOutput = new double[source.Count];
Er.Batch(source.Values, spanOutput, period);
// 4. Event-driven
var eventSource = new TSeries();
var eventIndicator = new Er(eventSource, period);
var eventResults = new double[source.Count];
for (int i = 0; i < source.Count; i++)
{
eventSource.Add(source[i]);
eventResults[i] = eventIndicator.Last.Value;
}
// Compare all modes
for (int i = 0; i < source.Count; i++)
{
Assert.Equal(streamResults[i], batchSeries.Values[i], Tolerance);
Assert.Equal(streamResults[i], spanOutput[i], Tolerance);
Assert.Equal(streamResults[i], eventResults[i], Tolerance);
}
}
// ───── G) Span API tests ─────
[Fact]
public void Batch_Span_MismatchedLength_Throws()
{
var src = new double[10];
var output = new double[5];
var ex = Assert.Throws<ArgumentException>(() => Er.Batch(src, output, 5));
Assert.Equal("output", ex.ParamName);
}
[Fact]
public void Batch_Span_InvalidPeriod_Throws()
{
var src = new double[10];
var output = new double[10];
var ex = Assert.Throws<ArgumentException>(() => Er.Batch(src, output, 0));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Batch_Span_Empty_NoException()
{
var src = ReadOnlySpan<double>.Empty;
var output = Span<double>.Empty;
Er.Batch(src, output, 5);
Assert.True(true); // S2699: assertion confirms no-exception completion
}
[Fact]
public void Batch_Span_MatchesTSeries()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
TSeries batchSeries = Er.Batch(source, 10);
var spanOutput = new double[source.Count];
Er.Batch(source.Values, spanOutput, 10);
for (int i = 0; i < source.Count; i++)
{
Assert.Equal(batchSeries.Values[i], spanOutput[i], Tolerance);
}
}
[Fact]
public void Batch_Span_NaN_Handled()
{
double[] src = [100, double.NaN, 102, 103, 104, 105, 106, 107, 108, 109];
var output = new double[src.Length];
Er.Batch(src, output, 5);
for (int i = 0; i < output.Length; i++)
{
Assert.True(double.IsFinite(output[i]));
}
}
// ───── H) Chainability ─────
[Fact]
public void Pub_Fires_OnUpdate()
{
var er = new Er(period: 5);
int fireCount = 0;
er.Pub += (object? _, in TValueEventArgs _) => fireCount++;
er.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(1, fireCount);
}
[Fact]
public void EventBased_Chaining_Works()
{
var source = new TSeries();
var er = new Er(source, period: 5);
source.Add(new TValue(DateTime.UtcNow, 100.0));
Assert.True(double.IsFinite(er.Last.Value));
}
}
+282
View File
@@ -0,0 +1,282 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// ER: Efficiency Ratio (Kaufman)
/// </summary>
/// <remarks>
/// Measures the signal-to-noise ratio of price movement over a lookback period.
/// ER = |Price Price[period]| / Σ|Price[i] Price[i1]| for i over period bars.
/// Output ranges from 0 (choppy/noisy) to 1 (perfectly trending).
///
/// Uses dual circular buffers with a running sum for O(1) per-bar updates:
/// - Close buffer (period+1): stores source values; signal = |newest oldest|
/// - Noise buffer (period): stores |bar-to-bar change|; noise = running sum
///
/// References:
/// Perry Kaufman, "Trading Systems and Methods", 1995
/// PineScript reference: er.pine
/// </remarks>
[SkipLocalsInit]
public sealed class Er : AbstractBase
{
private readonly int _period;
private readonly RingBuffer _closeBuf;
private readonly RingBuffer _noiseBuf;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double NoiseSum,
double PrevValue,
double LastValid,
int Count);
private State _state;
private State _p_state;
/// <summary>
/// Creates Efficiency Ratio indicator with specified period.
/// </summary>
/// <param name="period">Lookback period for efficiency measurement (must be &gt; 0)</param>
public Er(int period = 10)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
_closeBuf = new RingBuffer(period + 1);
_noiseBuf = new RingBuffer(period);
Name = $"Er({period})";
WarmupPeriod = period + 1;
}
/// <summary>
/// Creates Efficiency Ratio with specified source and period.
/// </summary>
public Er(ITValuePublisher source, int period = 10) : this(period)
{
source.Pub += Handle;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
/// <summary>
/// True if the indicator has enough data for valid results.
/// </summary>
public override bool IsHot => _closeBuf.IsFull;
/// <summary>
/// Period of the indicator.
/// </summary>
public int Period => _period;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
double value = input.Value;
// Sanitize input
if (!double.IsFinite(value))
{
value = double.IsFinite(_state.LastValid) ? _state.LastValid : 0.0;
}
else
{
_state.LastValid = value;
}
if (isNew)
{
_p_state = _state;
// Compute bar-to-bar absolute change
double absChange = double.IsFinite(_state.PrevValue) ? Math.Abs(value - _state.PrevValue) : 0.0;
// Update noise running sum: subtract oldest, add newest
if (_noiseBuf.IsFull)
{
_state.NoiseSum -= _noiseBuf[0];
}
_state.NoiseSum += absChange;
_noiseBuf.Add(absChange);
// Update close buffer
_closeBuf.Add(value);
_state.PrevValue = value;
_state.Count++;
}
else
{
_state = _p_state;
double absChange = double.IsFinite(_state.PrevValue) ? Math.Abs(value - _state.PrevValue) : 0.0;
if (_noiseBuf.IsFull)
{
_state.NoiseSum -= _noiseBuf[0];
}
_state.NoiseSum += absChange;
_noiseBuf.UpdateNewest(absChange);
_closeBuf.UpdateNewest(value);
_state.PrevValue = value;
_state.Count++;
}
// Signal = |current - oldest close|
double signal = _closeBuf.Count > _period
? Math.Abs(value - _closeBuf[0])
: 0.0;
// ER = signal / noise, clamped to [0, 1]
double er = _state.NoiseSum > 0.0 ? signal / _state.NoiseSum : 0.0;
er = Math.Clamp(er, 0.0, 1.0);
Last = new TValue(input.Time, er);
PubEvent(Last, isNew);
return Last;
}
/// <inheritdoc/>
public override TSeries Update(TSeries source)
{
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);
Batch(source.Values, vSpan, _period);
source.Times.CopyTo(tSpan);
for (int i = 0; i < len; i++)
{
Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
}
return new TSeries(t, v);
}
/// <inheritdoc/>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
TimeSpan interval = step ?? TimeSpan.FromTicks(1);
DateTime baseTime = DateTime.UtcNow - (interval * (source.Length - 1));
for (int i = 0; i < source.Length; i++)
{
Update(new TValue(baseTime + (interval * i), source[i]), isNew: true);
}
}
/// <inheritdoc/>
public override void Reset()
{
_closeBuf.Clear();
_noiseBuf.Clear();
_state = default;
_p_state = default;
Last = default;
}
/// <summary>
/// Calculates Efficiency Ratio for entire series.
/// </summary>
public static TSeries Batch(TSeries source, int period = 10)
{
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);
Batch(source.Values, vSpan, period);
source.Times.CopyTo(tSpan);
return new TSeries(t, v);
}
/// <summary>
/// Batch Efficiency Ratio calculation via dual circular buffers with running sum.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period = 10)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = source.Length;
if (len == 0)
{
return;
}
var closeBuf = new RingBuffer(period + 1);
var noiseBuf = new RingBuffer(period);
double noiseSum = 0.0;
double lastValid = 0.0;
double prevValue = double.NaN;
for (int i = 0; i < len; i++)
{
double val = source[i];
if (!double.IsFinite(val))
{
val = lastValid;
}
else
{
lastValid = val;
}
double absChange = double.IsFinite(prevValue) ? Math.Abs(val - prevValue) : 0.0;
prevValue = val;
// Update noise
if (noiseBuf.IsFull)
{
noiseSum -= noiseBuf[0];
}
noiseSum += absChange;
noiseBuf.Add(absChange);
// Update close
closeBuf.Add(val);
// Signal
double signal = closeBuf.Count > period
? Math.Abs(val - closeBuf[0])
: 0.0;
double er = noiseSum > 0.0 ? signal / noiseSum : 0.0;
output[i] = Math.Clamp(er, 0.0, 1.0);
}
}
/// <summary>
/// Creates an ER indicator, processes the source, and returns results with the indicator.
/// </summary>
public static (TSeries Results, Er Indicator) Calculate(TSeries source, int period = 10)
{
var indicator = new Er(period);
return (indicator.Update(source), indicator);
}
}
+172
View File
@@ -0,0 +1,172 @@
# ER: Efficiency Ratio
> "The best trades move in a straight line. The worst ones wander. ER tells you which kind you're looking at." -- Perry Kaufman
| Property | Value |
|----------|-------|
| **Category** | Oscillator |
| **Inputs** | Source (close) |
| **Parameters** | `period` (default 10) |
| **Outputs** | Single series (Efficiency Ratio) |
| **Output range** | $0$ to $1$ |
| **Warmup** | `period + 1` bars |
### Key takeaways
- Measures the signal-to-noise ratio of price movement: net directional change divided by total path length.
- Output of $1.0$ means price moved in a perfectly straight line (pure trend). Output of $0.0$ means all movement cancelled out (pure noise).
- Core component of Kaufman's Adaptive Moving Average (KAMA), where ER dynamically adjusts the smoothing constant.
- Uses dual circular buffers with a running noise sum for O(1) per-bar updates.
- Clamped to $[0, 1]$; division by zero (zero noise) returns $0$.
## Historical Context
Perry Kaufman introduced the Efficiency Ratio in *Trading Systems and Methods* (1995) as part of his Adaptive Moving Average (KAMA) framework. The idea was straightforward: an ideal trend indicator should react quickly in trending markets and slowly in choppy ones. ER provides the adaptive signal that tells KAMA how to behave.
The concept borrows from signal processing. Engineers measure signal-to-noise ratio to assess transmission quality. Kaufman applied the same logic to price: the "signal" is net directional movement, the "noise" is total bar-to-bar movement. A high ratio means the market is moving efficiently in one direction. A low ratio means the market is churning.
ER stands on its own as an oscillator, independent of KAMA. Traders use it to classify market regimes (trending vs. ranging) and to filter trade entries: take trend-following signals when ER is high, take mean-reversion signals when ER is low.
## What It Measures and Why It Matters
ER answers a specific question: over the last $N$ bars, how much of the total price movement was directional? If price rose 10 points but the cumulative absolute bar-to-bar changes totaled 10 points, the movement was perfectly efficient (ER = 1). If the cumulative changes totaled 100 points to achieve the same 10-point move, ER = 0.1.
This makes ER a regime classifier. High ER values (above 0.6) indicate strong directional trends with minimal retracement. Low ER values (below 0.3) indicate consolidation, choppy markets, or range-bound conditions. The crossover between these zones is where most adaptive strategies make their decisions.
Unlike ADX, which measures trend strength through directional movement calculations involving highs and lows, ER uses only closing prices and simple arithmetic. The simplicity is a feature: fewer assumptions, fewer opportunities for the math to mislead.
## Mathematical Foundation
### Core Formula
$$
\text{Signal}_t = |P_t - P_{t-N}|
$$
$$
\text{Noise}_t = \sum_{i=1}^{N} |P_i - P_{i-1}|
$$
$$
\text{ER}_t = \frac{\text{Signal}_t}{\text{Noise}_t}
$$
where:
- $P_t$ = current price (close)
- $N$ = lookback period (default 10)
- $\text{Signal}$ = absolute net price change over the period
- $\text{Noise}$ = sum of absolute bar-to-bar changes over the period
### Parameter Mapping
| Parameter | Symbol | Default | Constraint |
|-----------|--------|---------|------------|
| `period` | $N$ | 10 | $N \geq 1$ |
### Warmup Period
$$
W = N + 1
$$
The close buffer requires $N + 1$ values to compute $|P_t - P_{t-N}|$; the noise buffer requires $N$ absolute changes.
## Architecture & Physics
### 1. Dual Circular Buffer Design
Two `RingBuffer` instances provide O(1) per-bar computation:
- **Close buffer** (capacity $N + 1$): stores source values. Signal = $|\text{newest} - \text{oldest}|$.
- **Noise buffer** (capacity $N$): stores $|P_i - P_{i-1}|$ values. Running sum tracks total noise.
### 2. Running Noise Sum
Instead of re-summing $N$ absolute changes each bar, the implementation maintains a running sum: subtract the oldest absolute change (about to be evicted), add the newest. This reduces streaming complexity from O(N) to O(1).
### 3. State Management
A `record struct State` holds `NoiseSum`, `PrevValue`, `LastValid`, and `Count`. The `_state` / `_p_state` pattern supports bar correction: `isNew=true` snapshots state before advancing; `isNew=false` restores the snapshot and recomputes.
### 4. Edge Cases
| Condition | Behavior |
|-----------|----------|
| `period <= 0` | `ArgumentException` with `nameof(period)` |
| `NaN` / `Infinity` input | Substitutes last valid value |
| Zero noise (flat market) | Returns $0.0$ |
| Output domain | Clamped to $[0, 1]$ via `Math.Clamp` |
## Interpretation and Signals
### Signal Zones
| Zone | Condition | Interpretation |
|------|-----------|----------------|
| Strong trend | ER > 0.6 | Price moving efficiently; trend-following strategies favored |
| Moderate | 0.3 - 0.6 | Transitional; trend may be forming or fading |
| Choppy/Range | ER < 0.3 | High noise relative to direction; mean-reversion strategies favored |
### Signal Patterns
- **Regime filter**: Use ER > 0.5 as a gate for trend-following entries. Below 0.5, switch to range-bound strategies or stand aside.
- **KAMA integration**: Feed ER into Kaufman's smoothing constant formula: $\text{SC} = [\text{ER} \times (\text{fast} - \text{slow}) + \text{slow}]^2$.
- **Divergence**: Rising ER with declining price (or vice versa) can signal a new trend emerging from consolidation.
### Practical Notes
ER works best as a filter, not a standalone trading signal. Pair it with a trend indicator for direction (e.g., SMA slope) and use ER to decide how aggressively to follow that direction. Extremely low ER readings often precede breakouts, as tight consolidation compresses the noise.
## Related Indicators
- [**KAMA**](../../trends_IIR/kama/Kama.md): Kaufman's Adaptive Moving Average, which uses ER as its core input.
- [**Inertia**](../inertia/Inertia.md): Smoothed regression slope, another approach to measuring trend efficiency.
- [**Fisher**](../fisher/Fisher.md): Transforms price position into a Gaussian distribution, different approach to regime detection.
## Validation
| Library | Batch | Streaming | Span | Notes |
|---------|:-----:|:---------:|:----:|-------|
| **TA-Lib** | -- | -- | -- | No direct ER function |
| **Skender** | -- | -- | -- | Not available as standalone |
| **Tulip** | -- | -- | -- | Not available |
| **Ooples** | -- | -- | -- | Not available |
ER is validated indirectly through KAMA tests and internal consistency checks across all four API modes (batch, streaming, span, eventing).
## Performance Profile
### Key Optimizations
- **O(1) streaming**: Running noise sum avoids re-scanning the window each bar.
- **Zero allocation**: Pre-allocated `RingBuffer` instances and `record struct State`.
- **Dual buffer architecture**: Separates close history from noise history for independent O(1) access.
- **Aggressive inlining**: `Update` and `Batch` decorated with `[MethodImpl(AggressiveInlining)]`.
### Operation Count (Streaming Mode)
| Operation | Count per bar |
|-----------|---------------|
| ABS | 2 (signal + noise change) |
| SUB | 3 (signal diff, noise diff, running sum adjust) |
| ADD | 1 (running sum) |
| DIV | 1 (ER = signal/noise) |
| Clamp | 1 |
| NaN check | 1 |
| **Total** | **~9 ops** |
## Common Pitfalls
1. **Warmup requires $N + 1$ bars**: The close buffer needs one extra bar beyond the period to compute the net price change. `IsHot` becomes `true` when the close buffer is full.
2. **Zero noise does not mean trending**: Zero noise means price has not moved at all bar-to-bar over the window. ER returns $0$, not $1$. A perfectly flat market is not trending.
3. **ER is not directional**: ER = 0.8 tells you the market is trending efficiently. It does not tell you which direction. Always pair with a directional indicator.
4. **Short periods amplify noise**: Period $< 5$ makes ER excessively reactive. The default of 10 balances responsiveness and stability.
5. **Not suitable for mean-reversion entry**: ER measures efficiency, not overbought/oversold. Low ER signals a choppy market, not a reversal point.
6. **Bar correction cost**: `isNew=false` restores state and recomputes. Acceptable for infrequent corrections; not designed for high-frequency bar rewrites.
## References
- Kaufman, P. *Trading Systems and Methods*, 5th ed. John Wiley & Sons, 2013.
- Kaufman, P. *Smarter Trading: Improving Performance in Changing Markets*. McGraw-Hill, 1995.
- Achelis, S. B. *Technical Analysis from A to Z*. McGraw-Hill, 2000.
+72
View File
@@ -0,0 +1,72 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Efficiency Ratio (ER)", "ER", overlay=false)
//@function Calculates Kaufman's Efficiency Ratio as signal-to-noise measure
//@param source Series to calculate from
//@param period Lookback period for efficiency measurement
//@returns ER value (0 to 1) where 1 = trending, 0 = choppy
//@optimized O(1) per bar using dual circular buffers with running sum
er(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be greater than 0")
var int p = math.max(1, period)
// Close buffer: stores source values for signal (net change over period)
var array<float> closeBuf = array.new_float(p + 1, na)
var int closeHead = 0
var int closeCount = 0
// Noise buffer: stores |change| values for running sum
var array<float> noiseBuf = array.new_float(p, na)
var int noiseHead = 0
var float noiseSum = 0.0
var float prevVal = na
float current = nz(source)
// Compute bar-to-bar absolute change
float absChange = not na(prevVal) ? math.abs(current - prevVal) : 0.0
// Update noise running sum
float oldNoise = array.get(noiseBuf, noiseHead)
if not na(oldNoise)
noiseSum -= oldNoise
noiseSum += absChange
array.set(noiseBuf, noiseHead, absChange)
noiseHead := (noiseHead + 1) % p
// Update close buffer
if na(array.get(closeBuf, closeHead))
closeCount := math.min(closeCount + 1, p + 1)
array.set(closeBuf, closeHead, current)
// Get close from period bars ago
int oldIdx = (closeHead - p + p + 1) % (p + 1)
float oldClose = array.get(closeBuf, oldIdx)
closeHead := (closeHead + 1) % (p + 1)
prevVal := current
// Signal = |close - close[period]|
float signal = not na(oldClose) and closeCount > p ? math.abs(current - oldClose) : 0.0
// ER = signal / noise (0 when noise is 0)
float result = noiseSum != 0.0 ? signal / noiseSum : 0.0
math.max(0.0, math.min(1.0, result))
// ---------- Main loop ----------
// Inputs
i_source = input.source(close, "Source")
i_period = input.int(10, "Period", minval=1, maxval=500)
// Calculation
er_value = er(i_source, i_period)
// Plot
plot(er_value, "ER", color.new(color.yellow, 0), 2)
hline(0.5, "Midline", color=color.gray, linestyle=hline.style_dotted)