mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-27 17:27:43 +00:00
313 lines
9.1 KiB
C#
313 lines
9.1 KiB
C#
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace QuanTAlib;
|
|
|
|
/// <summary>
|
|
/// CG: Center of Gravity - Ehlers' oscillator that identifies potential turning points
|
|
/// in a time series by calculating the weighted center of mass of prices.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The Center of Gravity indicator, developed by John Ehlers, oscillates around zero
|
|
/// and provides early signals of potential reversals. It leads price movement,
|
|
/// making it useful for timing entries and exits.
|
|
///
|
|
/// Formula:
|
|
/// num = Σ(count * price[count-1]) for count = 1 to length
|
|
/// den = Σ(price[count-1]) for count = 1 to length
|
|
/// CG = (num / den) - (length + 1) / 2
|
|
///
|
|
/// Properties:
|
|
/// - Oscillates around zero
|
|
/// - Leads price movement (low lag)
|
|
/// - Positive values suggest downward pressure
|
|
/// - Negative values suggest upward pressure
|
|
/// - Zero crossings can signal turning points
|
|
///
|
|
/// Key Insight:
|
|
/// When prices are higher at the beginning of the window, CG is negative.
|
|
/// When prices are higher at the end of the window, CG is positive.
|
|
/// The indicator essentially measures where the "weight" of prices is concentrated.
|
|
/// </remarks>
|
|
[SkipLocalsInit]
|
|
public sealed class Cg : AbstractBase
|
|
{
|
|
private readonly int _period;
|
|
private readonly RingBuffer _buffer;
|
|
|
|
// Running sums for O(1) updates
|
|
private double _weightedSum;
|
|
private double _sum;
|
|
|
|
// Snapshot state for bar correction
|
|
private double _p_weightedSum;
|
|
private double _p_sum;
|
|
|
|
private int _updateCount;
|
|
private const int ResyncInterval = 1000;
|
|
|
|
public override bool IsHot => _buffer.IsFull;
|
|
|
|
/// <summary>
|
|
/// Creates a new Center of Gravity indicator.
|
|
/// </summary>
|
|
/// <param name="period">The lookback period for calculating CG (must be > 0).</param>
|
|
public Cg(int period = 10)
|
|
{
|
|
if (period < 1)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(period), "Period must be at least 1.");
|
|
}
|
|
|
|
_period = period;
|
|
_buffer = new RingBuffer(period);
|
|
Name = $"Cg({period})";
|
|
WarmupPeriod = period;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a chained Center of Gravity indicator.
|
|
/// </summary>
|
|
/// <param name="source">The source indicator to chain from.</param>
|
|
/// <param name="period">The lookback period.</param>
|
|
public Cg(ITValuePublisher source, int period = 10) : this(period)
|
|
{
|
|
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)
|
|
{
|
|
double value = input.Value;
|
|
if (!double.IsFinite(value))
|
|
{
|
|
value = _buffer.Count > 0 ? _buffer.Newest : 0;
|
|
}
|
|
|
|
if (isNew)
|
|
{
|
|
// Snapshot state for rollback
|
|
_p_weightedSum = _weightedSum;
|
|
_p_sum = _sum;
|
|
_buffer.Snapshot();
|
|
}
|
|
else
|
|
{
|
|
// Restore state from snapshot
|
|
_weightedSum = _p_weightedSum;
|
|
_sum = _p_sum;
|
|
_buffer.Restore();
|
|
}
|
|
|
|
// Add new value to buffer
|
|
_buffer.Add(value);
|
|
|
|
// Recalculate running sums
|
|
// Since the weights change position as we add values, we need to recalculate
|
|
// after each update (or track differential updates which is complex)
|
|
RecalculateSums();
|
|
|
|
if (isNew)
|
|
{
|
|
_updateCount++;
|
|
if (_updateCount % ResyncInterval == 0)
|
|
{
|
|
RecalculateSums(); // Already done above, but keeps pattern consistent
|
|
}
|
|
}
|
|
|
|
// Calculate CG
|
|
double cg = CalculateCg();
|
|
|
|
Last = new TValue(input.Time, cg);
|
|
PubEvent(Last, isNew);
|
|
return Last;
|
|
}
|
|
|
|
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);
|
|
|
|
Batch(source.Values, vSpan, _period);
|
|
source.Times.CopyTo(tSpan);
|
|
|
|
// Prime state with last 'period' values
|
|
int primeStart = Math.Max(0, len - _period);
|
|
for (int i = primeStart; i < len; i++)
|
|
{
|
|
Update(source[i]);
|
|
}
|
|
|
|
return new TSeries(t, v);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void RecalculateSums()
|
|
{
|
|
int n = _buffer.Count;
|
|
_weightedSum = 0;
|
|
_sum = 0;
|
|
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
double price = _buffer[i];
|
|
int weight = i + 1; // count = 1 to length (1-based weighting)
|
|
_weightedSum += weight * price;
|
|
_sum += price;
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private double CalculateCg()
|
|
{
|
|
int n = _buffer.Count;
|
|
if (n == 0 || _sum == 0) // skipcq: CS-R1077 - Exact-zero guard: _sum is cumulative price sum; zero means empty buffer, division by zero below
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// CG = (weightedSum / sum) - (n + 1) / 2
|
|
double centerOfMass = _weightedSum / _sum;
|
|
double midpoint = (n + 1) / 2.0;
|
|
return centerOfMass - midpoint;
|
|
}
|
|
|
|
public override void Reset()
|
|
{
|
|
_buffer.Clear();
|
|
_weightedSum = 0;
|
|
_sum = 0;
|
|
_p_weightedSum = 0;
|
|
_p_sum = 0;
|
|
_updateCount = 0;
|
|
Last = default;
|
|
}
|
|
|
|
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
|
{
|
|
foreach (double value in source)
|
|
{
|
|
Update(new TValue(DateTime.UtcNow, value));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates CG for a time series.
|
|
/// </summary>
|
|
public static TSeries Batch(TSeries source, int period = 10)
|
|
{
|
|
var cg = new Cg(period);
|
|
return cg.Update(source);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates CG in-place using a pre-allocated output span.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period = 10)
|
|
{
|
|
if (source.Length != output.Length)
|
|
{
|
|
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
|
}
|
|
|
|
if (period < 1)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(period), "Period must be at least 1.");
|
|
}
|
|
|
|
int len = source.Length;
|
|
if (len == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CalculateScalarCore(source, output, period);
|
|
}
|
|
|
|
public static (TSeries Results, Cg Indicator) Calculate(TSeries source, int period = 10)
|
|
{
|
|
var indicator = new Cg(period);
|
|
TSeries results = indicator.Update(source);
|
|
return (results, indicator);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void CalculateScalarCore(ReadOnlySpan<double> source, Span<double> output, int period)
|
|
{
|
|
int len = source.Length;
|
|
|
|
const int StackAllocThreshold = 256;
|
|
Span<double> buffer = period <= StackAllocThreshold
|
|
? stackalloc double[period]
|
|
: new double[period];
|
|
|
|
int bufferIndex = 0;
|
|
int bufferCount = 0;
|
|
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
double val = source[i];
|
|
if (!double.IsFinite(val))
|
|
{
|
|
val = bufferCount > 0 ? buffer[(bufferIndex - 1 + period) % period] : 0;
|
|
}
|
|
|
|
// Add to circular buffer
|
|
if (bufferCount < period)
|
|
{
|
|
buffer[bufferCount] = val;
|
|
bufferCount++;
|
|
}
|
|
else
|
|
{
|
|
buffer[bufferIndex] = val;
|
|
bufferIndex = (bufferIndex + 1) % period;
|
|
}
|
|
|
|
double weightedSum = 0;
|
|
double sum = 0;
|
|
|
|
// Calculate sums over the current buffer
|
|
int effectiveStart = bufferCount < period ? 0 : bufferIndex;
|
|
|
|
for (int j = 0; j < bufferCount; j++)
|
|
{
|
|
int idx = (effectiveStart + j) % period;
|
|
double price = buffer[idx];
|
|
int weight = j + 1; // 1-based weighting
|
|
weightedSum = Math.FusedMultiplyAdd(weight, price, weightedSum);
|
|
sum += price;
|
|
}
|
|
|
|
if (sum == 0) // skipcq: CS-R1077 - Exact-zero guard: sum is cumulative price sum; zero means empty buffer, division by zero below
|
|
{
|
|
output[i] = 0;
|
|
continue;
|
|
}
|
|
|
|
double centerOfMass = weightedSum / sum;
|
|
double midpoint = (bufferCount + 1) / 2.0;
|
|
output[i] = centerOfMass - midpoint;
|
|
}
|
|
}
|
|
} |