mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-01 19:27:44 +00:00
7253f61299
- 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.
316 lines
9.5 KiB
C#
316 lines
9.5 KiB
C#
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace QuanTAlib;
|
|
|
|
/// <summary>
|
|
/// CCYC: Ehlers Cyber Cycle — isolates the dominant cycle component from price data
|
|
/// using a 4-tap FIR pre-smoother and a 2-pole high-pass IIR filter.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// From John F. Ehlers, "Cybernetic Analysis for Stocks and Futures" (Wiley, 2004), Chapter 4.
|
|
///
|
|
/// Algorithm:
|
|
/// 1. 4-bar FIR smoother: smooth = (x + 2x[1] + 2x[2] + x[3]) / 6
|
|
/// Zeros at periods 2 and 3 eliminate aliased noise.
|
|
/// 2. 2-pole high-pass IIR:
|
|
/// cycle = c_hp * (smooth - 2*smooth[1] + smooth[2]) + c_fb1*cycle[1] + c_fb2*cycle[2]
|
|
/// where c_hp = (1-0.5*alpha)^2, c_fb1 = 2(1-alpha), c_fb2 = -(1-alpha)^2
|
|
/// 3. Bootstrap (bars < 7): cycle = (x - 2x[1] + x[2]) / 4
|
|
/// 4. Trigger = cycle[1] (one-bar delay for crossover signals)
|
|
///
|
|
/// Properties:
|
|
/// - O(1) per bar: 6 multiplications, 5 additions, 2 state variables
|
|
/// - Zero allocation in hot path
|
|
/// - Alpha controls high-pass cutoff: lower = smoother/more lag
|
|
/// - Trigger property provides the one-bar-delayed crossover line
|
|
/// </remarks>
|
|
[SkipLocalsInit]
|
|
public sealed class Ccyc : AbstractBase
|
|
{
|
|
private readonly double _chp; // (1 - 0.5*alpha)^2
|
|
private readonly double _cfb1; // 2*(1 - alpha)
|
|
private readonly double _cfb2; // -(1 - alpha)^2
|
|
|
|
[StructLayout(LayoutKind.Auto)]
|
|
private record struct State(
|
|
double Price0, double Price1, double Price2, double Price3,
|
|
double Smooth0, double Smooth1, double Smooth2,
|
|
double Cycle0, double Cycle1, double Cycle2,
|
|
int Count, double LastValid);
|
|
|
|
private State _s;
|
|
private State _ps;
|
|
|
|
/// <summary>One-bar-delayed cycle value for crossover detection.</summary>
|
|
public double Trigger { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public override bool IsHot => _s.Count >= WarmupPeriod;
|
|
|
|
/// <summary>
|
|
/// Creates a new Ccyc indicator with the specified alpha (damping factor).
|
|
/// </summary>
|
|
/// <param name="alpha">Damping factor controlling high-pass cutoff. Must be in (0, 1) exclusive. Default 0.07.</param>
|
|
public Ccyc(double alpha = 0.07)
|
|
{
|
|
if (alpha <= 0.0 || alpha >= 1.0)
|
|
{
|
|
throw new ArgumentException("Alpha must be between 0 and 1 (exclusive).", nameof(alpha));
|
|
}
|
|
|
|
double halfAlpha = 1.0 - 0.5 * alpha;
|
|
_chp = halfAlpha * halfAlpha;
|
|
double oneMinusAlpha = 1.0 - alpha;
|
|
_cfb1 = 2.0 * oneMinusAlpha;
|
|
_cfb2 = -(oneMinusAlpha * oneMinusAlpha);
|
|
|
|
Name = $"Ccyc({alpha:F2})";
|
|
WarmupPeriod = 7;
|
|
_s = default;
|
|
_ps = default;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new Ccyc indicator chained to a publisher source.
|
|
/// </summary>
|
|
/// <param name="source">Source indicator to subscribe to.</param>
|
|
/// <param name="alpha">Damping factor controlling high-pass cutoff. Default 0.07.</param>
|
|
public Ccyc(ITValuePublisher source, double alpha = 0.07) : this(alpha)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(source);
|
|
source.Pub += HandleInput;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void HandleInput(object? sender, in TValueEventArgs e)
|
|
{
|
|
Update(e.Value, e.IsNew);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public override TValue Update(TValue input, bool isNew = true)
|
|
{
|
|
// State management: save/restore for bar correction
|
|
if (isNew)
|
|
{
|
|
_ps = _s;
|
|
}
|
|
else
|
|
{
|
|
_s = _ps;
|
|
}
|
|
|
|
var s = _s;
|
|
|
|
double price = input.Value;
|
|
|
|
// NaN/Infinity guard: substitute last valid value
|
|
if (!double.IsFinite(price))
|
|
{
|
|
price = s.LastValid;
|
|
}
|
|
else
|
|
{
|
|
s = s with { LastValid = price };
|
|
}
|
|
|
|
// Increment bar count
|
|
int count = isNew ? s.Count + 1 : s.Count;
|
|
|
|
// Shift price history
|
|
double price3 = s.Price2;
|
|
double price2 = s.Price1;
|
|
double price1 = s.Price0;
|
|
double price0 = price;
|
|
|
|
// 4-tap FIR smoother: smooth = (x + 2*x1 + 2*x2 + x3) / 6
|
|
double smooth = (price0 + 2.0 * price1 + 2.0 * price2 + price3) / 6.0;
|
|
|
|
// Shift smooth history
|
|
double smooth2 = s.Smooth1;
|
|
double smooth1 = s.Smooth0;
|
|
double smooth0 = smooth;
|
|
|
|
double cycle;
|
|
if (count < 7)
|
|
{
|
|
// Bootstrap: second-difference of raw price
|
|
cycle = (price0 - 2.0 * price1 + price2) * 0.25;
|
|
}
|
|
else
|
|
{
|
|
// Steady-state: 2-pole high-pass IIR on smoothed input
|
|
// cycle = c_hp * (smooth - 2*smooth1 + smooth2) + c_fb1*cycle1 + c_fb2*cycle2
|
|
double diff = smooth0 - 2.0 * smooth1 + smooth2;
|
|
cycle = Math.FusedMultiplyAdd(_chp, diff,
|
|
Math.FusedMultiplyAdd(_cfb1, s.Cycle1, _cfb2 * s.Cycle2));
|
|
}
|
|
|
|
// Guard: if IIR diverges to non-finite, substitute zero
|
|
if (!double.IsFinite(cycle))
|
|
{
|
|
cycle = 0.0;
|
|
}
|
|
|
|
// Shift cycle history
|
|
double cycle2 = s.Cycle1;
|
|
double cycle1 = s.Cycle0;
|
|
double cycle0 = cycle;
|
|
|
|
// Trigger = previous cycle value
|
|
Trigger = cycle1;
|
|
|
|
_s = new State(
|
|
price0, price1, price2, price3,
|
|
smooth0, smooth1, smooth2,
|
|
cycle0, cycle1, cycle2,
|
|
count, s.LastValid);
|
|
|
|
Last = new TValue(input.Time, cycle);
|
|
PubEvent(Last, isNew);
|
|
return Last;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes a full TSeries, returning the cycle component for each bar.
|
|
/// </summary>
|
|
public override TSeries Update(TSeries source)
|
|
{
|
|
if (source.Count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
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);
|
|
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
var result = Update(source[i]);
|
|
vSpan[i] = result.Value;
|
|
}
|
|
source.Times.CopyTo(tSpan);
|
|
|
|
return new TSeries(t, v);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
|
{
|
|
foreach (double value in source)
|
|
{
|
|
Update(new TValue(DateTime.UtcNow, value));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Static batch: creates a Ccyc, processes source, returns output TSeries.
|
|
/// </summary>
|
|
public static TSeries Batch(TSeries source, double alpha = 0.07)
|
|
{
|
|
var ind = new Ccyc(alpha);
|
|
return ind.Update(source);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Static span-based batch: computes Cyber Cycle into output span.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double alpha = 0.07)
|
|
{
|
|
if (source.Length != output.Length)
|
|
{
|
|
throw new ArgumentException("Source and output must have the same length.", nameof(output));
|
|
}
|
|
if (alpha <= 0.0 || alpha >= 1.0)
|
|
{
|
|
throw new ArgumentException("Alpha must be between 0 and 1 (exclusive).", nameof(alpha));
|
|
}
|
|
|
|
int len = source.Length;
|
|
if (len == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
double halfAlpha = 1.0 - 0.5 * alpha;
|
|
double chp = halfAlpha * halfAlpha;
|
|
double oneMinusAlpha = 1.0 - alpha;
|
|
double cfb1 = 2.0 * oneMinusAlpha;
|
|
double cfb2 = -(oneMinusAlpha * oneMinusAlpha);
|
|
|
|
double price0 = 0, price1 = 0, price2 = 0, price3 = 0;
|
|
double smooth0 = 0, smooth1 = 0, smooth2 = 0;
|
|
double cycle0 = 0, cycle1 = 0, cycle2 = 0;
|
|
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
double val = source[i];
|
|
if (!double.IsFinite(val))
|
|
{
|
|
val = price0; // last valid
|
|
}
|
|
|
|
price3 = price2;
|
|
price2 = price1;
|
|
price1 = price0;
|
|
price0 = val;
|
|
|
|
double smooth = (price0 + 2.0 * price1 + 2.0 * price2 + price3) / 6.0;
|
|
smooth2 = smooth1;
|
|
smooth1 = smooth0;
|
|
smooth0 = smooth;
|
|
|
|
double cycle;
|
|
int barNum = i + 1;
|
|
if (barNum < 7)
|
|
{
|
|
cycle = (price0 - 2.0 * price1 + price2) * 0.25;
|
|
}
|
|
else
|
|
{
|
|
double diff = smooth0 - 2.0 * smooth1 + smooth2;
|
|
cycle = Math.FusedMultiplyAdd(chp, diff,
|
|
Math.FusedMultiplyAdd(cfb1, cycle1, cfb2 * cycle2));
|
|
}
|
|
|
|
// Guard: if IIR diverges to non-finite, substitute zero
|
|
if (!double.IsFinite(cycle))
|
|
{
|
|
cycle = 0.0;
|
|
}
|
|
|
|
cycle2 = cycle1;
|
|
cycle1 = cycle0;
|
|
cycle0 = cycle;
|
|
output[i] = cycle;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Static convenience method: returns (TSeries results, Ccyc indicator) for inspection.
|
|
/// </summary>
|
|
public static (TSeries Results, Ccyc Indicator) Calculate(TSeries source, double alpha = 0.07)
|
|
{
|
|
var ind = new Ccyc(alpha);
|
|
var results = ind.Update(source);
|
|
return (results, ind);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Reset()
|
|
{
|
|
_s = default;
|
|
_ps = default;
|
|
Last = default;
|
|
Trigger = 0;
|
|
}
|
|
}
|