mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-21 20:18:05 +00:00
Deep review of all indicator categories verified .md headers against .cs WarmupPeriod, parameters, inputs, and outputs. Fixes include warmup corrections, parameter documentation, output type accuracy, and Pine Script alignment.
197 lines
5.6 KiB
C#
197 lines
5.6 KiB
C#
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace QuanTAlib;
|
|
|
|
/// <summary>
|
|
/// DECAY: Linear Decay
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Tracks the maximum of the current input and the previous output minus a fixed
|
|
/// step of 1/period per bar. When price is rising or flat the output follows price;
|
|
/// when price drops the output decays linearly toward it.
|
|
///
|
|
/// Calculation: <c>output = max(input, prev_output - 1/period)</c>.
|
|
/// Origin: Tulip Indicators (ti_decay).
|
|
/// </remarks>
|
|
/// <seealso href="Decay.md">Detailed documentation</seealso>
|
|
[SkipLocalsInit]
|
|
public sealed class Decay : AbstractBase
|
|
{
|
|
private readonly double _scale;
|
|
private int _count;
|
|
[StructLayout(LayoutKind.Auto)]
|
|
private record struct State(double LastValid, double LastOutput);
|
|
private State _state, _p_state;
|
|
private int _p_count;
|
|
private ITValuePublisher? _source;
|
|
private bool _disposed;
|
|
|
|
public override bool IsHot => _count > 0;
|
|
|
|
/// <summary>
|
|
/// Initializes a new Linear Decay indicator with specified period.
|
|
/// </summary>
|
|
/// <param name="period">Decay period (must be >= 1)</param>
|
|
public Decay(int period = 5)
|
|
{
|
|
if (period < 1)
|
|
{
|
|
throw new ArgumentException("Period must be >= 1", nameof(period));
|
|
}
|
|
|
|
_scale = 1.0 / period;
|
|
Name = $"Decay({period})";
|
|
WarmupPeriod = 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new Linear Decay indicator with source for event-based chaining.
|
|
/// </summary>
|
|
/// <param name="source">Source indicator for chaining</param>
|
|
/// <param name="period">Decay period</param>
|
|
public Decay(ITValuePublisher source, int period = 5) : this(period)
|
|
{
|
|
_source = source;
|
|
_source.Pub += HandleUpdate;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public override TValue Update(TValue input, bool isNew = true)
|
|
{
|
|
if (isNew)
|
|
{
|
|
_p_state = _state;
|
|
_p_count = _count;
|
|
}
|
|
else
|
|
{
|
|
_state = _p_state;
|
|
_count = _p_count;
|
|
}
|
|
|
|
double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid;
|
|
|
|
double result;
|
|
if (_count == 0)
|
|
{
|
|
result = value;
|
|
}
|
|
else
|
|
{
|
|
double decayed = _state.LastOutput - _scale;
|
|
result = value > decayed ? value : decayed;
|
|
}
|
|
|
|
_state = new State(value, result);
|
|
if (isNew)
|
|
{
|
|
_count++;
|
|
}
|
|
|
|
Last = new TValue(input.Time, result);
|
|
PubEvent(Last, isNew);
|
|
return Last;
|
|
}
|
|
|
|
public override TSeries Update(TSeries source)
|
|
{
|
|
var result = new TSeries(source.Count);
|
|
ReadOnlySpan<double> values = source.Values;
|
|
ReadOnlySpan<long> times = source.Times;
|
|
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
var tv = Update(new TValue(new DateTime(times[i], DateTimeKind.Utc), values[i]), true);
|
|
result.Add(tv, true);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
|
{
|
|
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
|
|
DateTime time = DateTime.UtcNow - (interval * source.Length);
|
|
|
|
for (int i = 0; i < source.Length; i++)
|
|
{
|
|
Update(new TValue(time, source[i]), true);
|
|
time += interval;
|
|
}
|
|
}
|
|
|
|
public static TSeries Batch(TSeries source, int period = 5)
|
|
{
|
|
var indicator = new Decay(period);
|
|
return indicator.Update(source);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates linear decay over a span of values. Zero-allocation.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
|
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period = 5)
|
|
{
|
|
if (source.Length == 0)
|
|
{
|
|
throw new ArgumentException("Source cannot be empty", nameof(source));
|
|
}
|
|
|
|
if (output.Length < source.Length)
|
|
{
|
|
throw new ArgumentException("Output length must be >= source length", nameof(output));
|
|
}
|
|
|
|
if (period < 1)
|
|
{
|
|
throw new ArgumentException("Period must be >= 1", nameof(period));
|
|
}
|
|
|
|
double scale = 1.0 / period;
|
|
ref double srcRef = ref MemoryMarshal.GetReference(source);
|
|
ref double outRef = ref MemoryMarshal.GetReference(output);
|
|
|
|
Unsafe.Add(ref outRef, 0) = Unsafe.Add(ref srcRef, 0);
|
|
|
|
for (int i = 1; i < source.Length; i++)
|
|
{
|
|
double d = Unsafe.Add(ref outRef, i - 1) - scale;
|
|
double s = Unsafe.Add(ref srcRef, i);
|
|
Unsafe.Add(ref outRef, i) = s > d ? s : d;
|
|
}
|
|
}
|
|
|
|
public static (TSeries Results, Decay Indicator) Calculate(TSeries source, int period = 5)
|
|
{
|
|
var indicator = new Decay(period);
|
|
TSeries results = indicator.Update(source);
|
|
return (results, indicator);
|
|
}
|
|
|
|
public override void Reset()
|
|
{
|
|
_count = 0;
|
|
_p_count = 0;
|
|
_state = default;
|
|
_p_state = default;
|
|
Last = default;
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
if (disposing && _source != null)
|
|
{
|
|
_source.Pub -= HandleUpdate;
|
|
_source = null;
|
|
}
|
|
_disposed = true;
|
|
}
|
|
base.Dispose(disposing);
|
|
}
|
|
}
|