mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-08 05:57:43 +00:00
382 lines
12 KiB
C#
382 lines
12 KiB
C#
using System.Runtime.CompilerServices;
|
||
using System.Runtime.InteropServices;
|
||
|
||
namespace QuanTAlib;
|
||
|
||
/// <summary>
|
||
/// KAMA: Kaufman's Adaptive Moving Average
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Adapts smoothing based on efficiency ratio (signal/noise) to reduce whipsaws in ranging markets.
|
||
/// Faster in trends, slower during consolidation.
|
||
///
|
||
/// Calculation: <c>ER = |Change|/Volatility; SC = (ER×(αfast-αslow)+αslow)²; KAMA += SC×(P-KAMA)</c>.
|
||
/// </remarks>
|
||
/// <seealso href="Kama.md">Detailed documentation</seealso>
|
||
/// <seealso href="kama.pine">Reference Pine Script implementation</seealso>
|
||
[SkipLocalsInit]
|
||
public sealed class Kama : AbstractBase
|
||
{
|
||
private readonly double _fastAlpha;
|
||
private readonly double _slowAlpha;
|
||
private readonly RingBuffer _buffer;
|
||
private readonly TValuePublishedHandler _handler;
|
||
|
||
[StructLayout(LayoutKind.Auto)]
|
||
private record struct State(double Kama, double VolatilitySum, double NextDiffOut, double LastValidValue);
|
||
private State _state;
|
||
private State _p_state;
|
||
|
||
public override bool IsHot => _buffer.IsFull;
|
||
|
||
/// <summary>
|
||
/// Creates KAMA with specified parameters.
|
||
/// </summary>
|
||
/// <param name="period">Lookback period for Efficiency Ratio (default 10).</param>
|
||
/// <param name="fastPeriod">Fast EMA period for SC calculation (default 2).</param>
|
||
/// <param name="slowPeriod">Slow EMA period for SC calculation (default 30).</param>
|
||
public Kama(int period = 10, int fastPeriod = 2, int slowPeriod = 30)
|
||
{
|
||
if (period <= 0)
|
||
{
|
||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||
}
|
||
|
||
if (fastPeriod <= 0)
|
||
{
|
||
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
|
||
}
|
||
|
||
if (slowPeriod <= 0)
|
||
{
|
||
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
|
||
}
|
||
|
||
if (fastPeriod >= slowPeriod)
|
||
{
|
||
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
|
||
}
|
||
|
||
// Buffer needs to hold period + 1 values to calculate Change over 'period' bars
|
||
// Change = Price[0] - Price[period]
|
||
_buffer = new RingBuffer(period + 1);
|
||
|
||
_fastAlpha = 2.0 / (fastPeriod + 1);
|
||
_slowAlpha = 2.0 / (slowPeriod + 1);
|
||
_handler = Handle;
|
||
|
||
Name = $"Kama({period}, {fastPeriod}, {slowPeriod})";
|
||
WarmupPeriod = period + 1;
|
||
|
||
_state.Kama = double.NaN;
|
||
_state.LastValidValue = double.NaN;
|
||
_p_state.Kama = double.NaN;
|
||
_p_state.LastValidValue = double.NaN;
|
||
}
|
||
|
||
public Kama(ITValuePublisher source, int period = 10, int fastPeriod = 2, int slowPeriod = 30)
|
||
: this(period, fastPeriod, slowPeriod)
|
||
{
|
||
source.Pub += _handler;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private double GetValidValue(double input)
|
||
{
|
||
if (double.IsFinite(input))
|
||
{
|
||
_state.LastValidValue = input;
|
||
return input;
|
||
}
|
||
return _state.LastValidValue;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public override TValue Update(TValue input, bool isNew = true)
|
||
{
|
||
if (isNew)
|
||
{
|
||
_p_state = _state;
|
||
}
|
||
else
|
||
{
|
||
_state = _p_state;
|
||
}
|
||
|
||
double val = GetValidValue(input.Value);
|
||
if (double.IsNaN(val))
|
||
{
|
||
Last = new TValue(input.Time, double.NaN);
|
||
PubEvent(Last);
|
||
return Last;
|
||
}
|
||
|
||
if (isNew)
|
||
{
|
||
bool wasFull = _buffer.IsFull;
|
||
_buffer.Add(val);
|
||
|
||
if (wasFull)
|
||
{
|
||
double diff_out = _p_state.NextDiffOut;
|
||
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]);
|
||
_state.VolatilitySum += diff_in - diff_out;
|
||
|
||
// Calculate NextDiffOut for the next step
|
||
// NextDiffOut = abs(buffer[0] - buffer[1])
|
||
_state.NextDiffOut = Math.Abs(_buffer[0] - _buffer[1]);
|
||
}
|
||
else if (_buffer.Count >= 2)
|
||
{
|
||
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]);
|
||
_state.VolatilitySum += diff_in;
|
||
|
||
if (_buffer.IsFull)
|
||
{
|
||
// Buffer just became full.
|
||
// NextDiffOut = abs(buffer[0] - buffer[1])
|
||
_state.NextDiffOut = Math.Abs(_buffer[0] - _buffer[1]);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_buffer.UpdateNewest(val);
|
||
|
||
if (_buffer.IsFull)
|
||
{
|
||
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]);
|
||
// Use NextDiffOut from _p_state (which is the correct DiffOut for this transition)
|
||
_state.VolatilitySum = _p_state.VolatilitySum + diff_in - _p_state.NextDiffOut;
|
||
}
|
||
else if (_buffer.Count >= 2)
|
||
{
|
||
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]);
|
||
_state.VolatilitySum = _p_state.VolatilitySum + diff_in;
|
||
}
|
||
}
|
||
|
||
// Calculate KAMA
|
||
if (double.IsNaN(_state.Kama))
|
||
{
|
||
_state.Kama = val;
|
||
}
|
||
else
|
||
{
|
||
double change = Math.Abs(_buffer[^1] - _buffer[0]);
|
||
double volatility = _state.VolatilitySum;
|
||
|
||
// Avoid division by zero
|
||
double er = (volatility > 1e-10) ? change / volatility : 0.0;
|
||
// Cap ER at 1.0 just in case floating point errors push it slightly over
|
||
if (er > 1.0)
|
||
{
|
||
er = 1.0;
|
||
}
|
||
|
||
// double sc = er * (_fastAlpha - _slowAlpha) + _slowAlpha; // skipcq: S125
|
||
double sc = Math.FusedMultiplyAdd(er, _fastAlpha - _slowAlpha, _slowAlpha);
|
||
sc *= sc;
|
||
|
||
double prevKama = _p_state.Kama;
|
||
if (double.IsNaN(prevKama))
|
||
{
|
||
prevKama = _state.Kama;
|
||
}
|
||
|
||
// _state.Kama = prevKama + sc * (val - prevKama); // skipcq: S125
|
||
_state.Kama = Math.FusedMultiplyAdd(sc, val - prevKama, prevKama);
|
||
}
|
||
|
||
Last = new TValue(input.Time, _state.Kama);
|
||
PubEvent(Last, isNew);
|
||
return Last;
|
||
}
|
||
|
||
public override TSeries Update(TSeries source)
|
||
{
|
||
if (source.Count == 0)
|
||
{
|
||
return new TSeries([], []);
|
||
}
|
||
|
||
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);
|
||
|
||
source.Times.CopyTo(tSpan);
|
||
|
||
Reset();
|
||
for (int i = 0; i < len; i++)
|
||
{
|
||
vSpan[i] = Update(new TValue(source.Times[i], source.Values[i])).Value;
|
||
}
|
||
|
||
return new TSeries(t, v);
|
||
}
|
||
|
||
private void Handle(object? sender, in TValueEventArgs args)
|
||
{
|
||
Update(args.Value, args.IsNew);
|
||
}
|
||
|
||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||
{
|
||
foreach (var value in source)
|
||
{
|
||
Update(new TValue(DateTime.MinValue, value));
|
||
}
|
||
}
|
||
|
||
public static TSeries Batch(TSeries source, int period, int fastPeriod = 2, int slowPeriod = 30)
|
||
{
|
||
var kama = new Kama(period, fastPeriod, slowPeriod);
|
||
return kama.Update(source);
|
||
}
|
||
|
||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, int fastPeriod = 2, int slowPeriod = 30)
|
||
{
|
||
if (period <= 0)
|
||
{
|
||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||
}
|
||
|
||
if (fastPeriod <= 0)
|
||
{
|
||
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
|
||
}
|
||
|
||
if (slowPeriod <= 0)
|
||
{
|
||
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
|
||
}
|
||
|
||
if (fastPeriod >= slowPeriod)
|
||
{
|
||
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
|
||
}
|
||
|
||
if (source.Length != output.Length)
|
||
{
|
||
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
||
}
|
||
|
||
double fastAlpha = 2.0 / (fastPeriod + 1);
|
||
double slowAlpha = 2.0 / (slowPeriod + 1);
|
||
|
||
// We need a buffer for price history to calculate ER
|
||
// Size period + 1
|
||
int bufSize = period + 1;
|
||
Span<double> buffer = bufSize <= 256 ? stackalloc double[bufSize] : new double[bufSize];
|
||
int bufferIdx = 0;
|
||
int count = 0;
|
||
|
||
double volatilitySum = 0;
|
||
double kama = 0;
|
||
bool kamaInitialized = false;
|
||
double lastValid = double.NaN;
|
||
|
||
for (int i = 0; i < source.Length; i++)
|
||
{
|
||
double val = source[i];
|
||
if (double.IsFinite(val))
|
||
{
|
||
lastValid = val;
|
||
}
|
||
else
|
||
{
|
||
val = lastValid;
|
||
}
|
||
|
||
if (double.IsNaN(val))
|
||
{
|
||
output[i] = double.NaN;
|
||
continue;
|
||
}
|
||
|
||
// Add to buffer
|
||
double removed = buffer[bufferIdx];
|
||
buffer[bufferIdx] = val;
|
||
|
||
// Update volatility
|
||
if (count >= 1)
|
||
{
|
||
// diff_in = abs(val - prev)
|
||
// prev is at bufferIdx-1 (circular)
|
||
int prevIdx = (bufferIdx - 1 + bufSize) % bufSize;
|
||
double diff_in = Math.Abs(val - buffer[prevIdx]);
|
||
|
||
volatilitySum += diff_in;
|
||
|
||
if (count == bufSize)
|
||
{
|
||
// diff_out = abs(removed - new_oldest)
|
||
// new_oldest is at (bufferIdx + 1) % bufSize
|
||
int oldestIdx = (bufferIdx + 1) % bufSize;
|
||
double diff_out = Math.Abs(removed - buffer[oldestIdx]);
|
||
volatilitySum -= diff_out;
|
||
}
|
||
}
|
||
|
||
bufferIdx = (bufferIdx + 1) % bufSize;
|
||
if (count < bufSize)
|
||
{
|
||
count++;
|
||
}
|
||
|
||
if (!kamaInitialized)
|
||
{
|
||
kama = val;
|
||
kamaInitialized = true;
|
||
output[i] = kama;
|
||
}
|
||
else
|
||
{
|
||
// Calculate ER
|
||
// Change = abs(current - oldest)
|
||
// current = val (just written to buffer at index (bufferIdx - 1 + bufSize) % bufSize)
|
||
// oldest: when full, oldest is at bufferIdx (the next write position)
|
||
// Note: bufferIdx has already been advanced, so current value is at (bufferIdx - 1 + bufSize) % bufSize
|
||
|
||
// When full, oldest is at bufferIdx (next write position); when not full, oldest is at index 0
|
||
double change = Math.Abs(val - buffer[count == bufSize ? bufferIdx : 0]);
|
||
|
||
double er = (volatilitySum > 1e-10) ? change / volatilitySum : 0.0;
|
||
if (er > 1.0)
|
||
{
|
||
er = 1.0;
|
||
}
|
||
|
||
// double sc = er * (fastAlpha - slowAlpha) + slowAlpha; // skipcq: S125
|
||
double sc = Math.FusedMultiplyAdd(er, fastAlpha - slowAlpha, slowAlpha);
|
||
sc *= sc;
|
||
|
||
// kama += sc * (val - kama); // skipcq: S125
|
||
kama = Math.FusedMultiplyAdd(sc, val - kama, kama);
|
||
output[i] = kama;
|
||
}
|
||
}
|
||
}
|
||
|
||
public static (TSeries Results, Kama Indicator) Calculate(TSeries source, int period, int fastPeriod = 2, int slowPeriod = 30)
|
||
{
|
||
var indicator = new Kama(period, fastPeriod, slowPeriod);
|
||
TSeries results = indicator.Update(source);
|
||
return (results, indicator);
|
||
}
|
||
|
||
public override void Reset()
|
||
{
|
||
_buffer.Clear();
|
||
_state = default;
|
||
_state.Kama = double.NaN;
|
||
_state.LastValidValue = double.NaN;
|
||
_p_state = _state;
|
||
Last = default;
|
||
}
|
||
} |