Files
QuanTAlib/lib/trends/kama/Kama.cs
T
Miha Kralj 5c3b3fbab4 Refactor indicators to support optional time step in Prime method
- Updated the Prime method signature in multiple indicators (Jma, Kama, Lsma, Mama, Mgdi, Pwma, Rma, Sma, Ssf, Super, T3, Tema, Trima, Usf, Vidya, Wma, Atr) to accept an optional TimeSpan parameter for improved flexibility.
- Added unit tests for Lsma to verify Dispose functionality, ensuring proper unsubscription from the source and thread safety.
- Enhanced Mama and Wma classes to handle non-finite inputs gracefully and added checks for valid parameters in constructors.
- Introduced additional tests for T3 to validate constructor behavior with invalid volume factors.
- Ensured all indicators maintain consistent behavior when handling edge cases, such as empty buffers and non-finite values.
2025-12-28 15:14:07 -08:00

338 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// KAMA: Kaufman's Adaptive Moving Average
/// </summary>
/// <remarks>
/// KAMA adapts to market volatility by adjusting its smoothing factor based on an Efficiency Ratio (ER).
/// ER is calculated as the ratio of the absolute price change over a period to the sum of absolute price changes (volatility).
///
/// Formula:
/// ER = Change / Volatility
/// Change = Abs(Price - Price[period])
/// Volatility = Sum(Abs(Price[i] - Price[i-1]), period)
/// SC = (ER * (fast_alpha - slow_alpha) + slow_alpha)^2
/// KAMA = KAMA[prev] + SC * (Price - KAMA[prev])
/// </remarks>
[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, 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 Calculate(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
// oldest:
// if full, oldest is at bufferIdx (which is the next write pos, so it holds the oldest)
// Wait, bufferIdx points to where we WILL write next.
// So buffer[bufferIdx] is the oldest value (the one that will be overwritten next).
// So Change = abs(val - buffer[bufferIdx])
double change = 0;
change = (count == bufSize) ? Math.Abs(val - buffer[bufferIdx]) : Math.Abs(val - buffer[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 override void Reset()
{
_buffer.Clear();
_state = default;
_state.Kama = double.NaN;
_state.LastValidValue = double.NaN;
_p_state = _state;
Last = default;
}
}