mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-15 17:18:05 +00:00
- Implemented the TRAMA (Trend Regularity Adaptive Moving Average) class with adaptive EMA logic. - Added unit tests for TRAMA functionality, including constructor validation, basic calculations, state management, and robustness checks. - Created validation tests to ensure consistency across different modes of operation (streaming, batch, and static calculations). - Enhanced documentation for TRAMA, including performance profiles and quality metrics. - Updated workspace configuration by removing unnecessary folder references.
237 lines
6.1 KiB
C#
237 lines
6.1 KiB
C#
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace QuanTAlib;
|
|
|
|
/// <summary>
|
|
/// AGC: Automatic Gain Control (Ehlers)
|
|
/// Amplitude normalization via exponential peak tracking. Normalizes any oscillating
|
|
/// input signal to the [-1, +1] range by dividing by a decaying running peak.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The algorithm is based on a Pine Script implementation:
|
|
/// https://github.com/mihakralj/pinescript/blob/main/indicators/numerics/agc.md
|
|
///
|
|
/// Key properties:
|
|
/// - Pure normalizer: does NOT contain an internal filter stage
|
|
/// - Input should oscillate around zero (use after a bandpass/roofing/SSF filter)
|
|
/// - Peak decays exponentially each bar (decay=0.991 ≈ 110-bar half-life)
|
|
/// - Peak ratchets up instantly when |input| exceeds decayed peak
|
|
/// - Output bounded to [-1, +1] for well-behaved oscillating inputs
|
|
///
|
|
/// Complexity: O(1) — one multiply, one compare, one divide per bar
|
|
/// </remarks>
|
|
[SkipLocalsInit]
|
|
public sealed class Agc : AbstractBase
|
|
{
|
|
private readonly double _decay;
|
|
private ITValuePublisher? _publisher;
|
|
private TValuePublishedHandler? _handler;
|
|
private bool _isNew;
|
|
|
|
[StructLayout(LayoutKind.Auto)]
|
|
private record struct State
|
|
{
|
|
public double Peak;
|
|
public double LastValid;
|
|
public int Count;
|
|
}
|
|
|
|
private State _state;
|
|
private State _p_state;
|
|
|
|
/// <summary>
|
|
/// Peak decay factor per bar. Controls how quickly the normalizer adapts
|
|
/// to decreasing amplitude. 0.991 ≈ 110-bar half-life.
|
|
/// </summary>
|
|
public double Decay => _decay;
|
|
|
|
public bool IsNew => _isNew;
|
|
public override bool IsHot => _state.Count > 0;
|
|
|
|
public Agc(double decay = 0.991)
|
|
{
|
|
if (decay is <= 0.0 or >= 1.0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(decay), "Decay must be between 0 and 1 exclusive.");
|
|
}
|
|
|
|
_decay = decay;
|
|
Name = $"AGC({decay:F3})";
|
|
WarmupPeriod = 1;
|
|
_state.Peak = 1e-10; // tiny positive to avoid div-by-zero on first bar
|
|
_state.LastValid = 0.0;
|
|
}
|
|
|
|
public Agc(ITValuePublisher source, double decay = 0.991) : this(decay)
|
|
{
|
|
_publisher = source;
|
|
_handler = Handle;
|
|
source.Pub += _handler;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void Handle(object? sender, in TValueEventArgs args)
|
|
{
|
|
Update(args.Value, args.IsNew);
|
|
}
|
|
|
|
public override TSeries Update(TSeries source)
|
|
{
|
|
if (source.Count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
double[] values = source.Values.ToArray();
|
|
double[] results = new double[values.Length];
|
|
|
|
Batch(values, results, _decay);
|
|
|
|
TSeries output = [];
|
|
for (int i = 0; i < values.Length; i++)
|
|
{
|
|
output.Add(source[i].Time, results[i]);
|
|
}
|
|
|
|
// Sync internal state by replaying
|
|
Reset();
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
Update(source[i]);
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public override TValue Update(TValue input, bool isNew = true)
|
|
{
|
|
_isNew = isNew;
|
|
if (isNew)
|
|
{
|
|
_p_state = _state;
|
|
}
|
|
else
|
|
{
|
|
_state = _p_state;
|
|
}
|
|
|
|
var s = _state;
|
|
|
|
// Handle bad data
|
|
double val = input.Value;
|
|
if (!double.IsFinite(val))
|
|
{
|
|
val = double.IsFinite(s.LastValid) ? s.LastValid : 0.0;
|
|
}
|
|
else
|
|
{
|
|
s.LastValid = val;
|
|
}
|
|
|
|
// Exponential peak decay
|
|
s.Peak *= _decay;
|
|
|
|
// Ratchet up when signal exceeds decayed peak
|
|
double absVal = Math.Abs(val);
|
|
if (absVal > s.Peak)
|
|
{
|
|
s.Peak = absVal;
|
|
}
|
|
|
|
// Normalize: output = val / peak
|
|
double result = s.Peak > 0.0 ? val / s.Peak : 0.0;
|
|
|
|
if (isNew)
|
|
{
|
|
s.Count++;
|
|
}
|
|
|
|
_state = s;
|
|
|
|
Last = new TValue(input.Time, result);
|
|
PubEvent(Last, isNew);
|
|
return Last;
|
|
}
|
|
|
|
public static TSeries Batch(TSeries source, double decay = 0.991)
|
|
{
|
|
var indicator = new Agc(decay);
|
|
return indicator.Update(source);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double decay = 0.991)
|
|
{
|
|
if (source.Length != output.Length)
|
|
{
|
|
throw new ArgumentException("Source and output spans must be of the same length.", nameof(output));
|
|
}
|
|
|
|
double peak = 1e-10;
|
|
double lastValid = 0.0;
|
|
|
|
for (int i = 0; i < source.Length; i++)
|
|
{
|
|
double val = source[i];
|
|
if (!double.IsFinite(val))
|
|
{
|
|
val = lastValid;
|
|
}
|
|
else
|
|
{
|
|
lastValid = val;
|
|
}
|
|
|
|
// Decay peak
|
|
peak *= decay;
|
|
|
|
// Ratchet
|
|
double absVal = Math.Abs(val);
|
|
if (absVal > peak)
|
|
{
|
|
peak = absVal;
|
|
}
|
|
|
|
// Normalize
|
|
output[i] = peak > 0.0 ? val / peak : 0.0;
|
|
}
|
|
}
|
|
|
|
public override void Reset()
|
|
{
|
|
_state = default;
|
|
_state.Peak = 1e-10;
|
|
_state.LastValid = 0.0;
|
|
_p_state = default;
|
|
Last = default;
|
|
}
|
|
|
|
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
|
{
|
|
foreach (double val in source)
|
|
{
|
|
Update(new TValue(DateTime.UtcNow, val), isNew: true);
|
|
}
|
|
}
|
|
|
|
public static (TSeries Results, Agc Indicator) Calculate(TSeries source, double decay = 0.991)
|
|
{
|
|
var indicator = new Agc(decay);
|
|
TSeries results = indicator.Update(source);
|
|
return (results, indicator);
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing && _publisher != null && _handler != null)
|
|
{
|
|
_publisher.Pub -= _handler;
|
|
_publisher = null;
|
|
_handler = null;
|
|
}
|
|
base.Dispose(disposing);
|
|
}
|
|
}
|