Files
2026-02-10 21:33:16 -08:00

382 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}