Files
Miha Kralj 653aafacd8 feat: Add Prime method to various indicators for initializing state with historical data
- Implemented Prime method in Vel, Ao, Apo, Frama, Adl, Adosc, Aobv, Cmf, Efi, Eom, Iii, Kvo, Mfi, Nvi, Obv, Pvd, Pvi, Pvo, Pvr, Pvt, Tvi, Twap, Va, Vf, Vo, Vroc, Vwad, Vwap, and Vwma classes.
- The Prime method resets the indicator state and processes the provided historical bar data to initialize the indicator.
- Added warmup period property to Adl and Wad classes to define the minimum number of data points required for validity.
- Updated benchmark tests to use Batch methods for performance evaluation.
2026-02-11 20:38:38 -08:00

311 lines
8.9 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.Numerics;
namespace QuanTAlib;
/// <summary>
/// CMF: Chaikin Money Flow
/// </summary>
/// <remarks>
/// Measures buying/selling pressure via close position within range and volume.
/// Oscillates between -1 and +1; positive indicates accumulation, negative indicates distribution.
///
/// Calculation: <c>MFM = [(Close - Low) - (High - Close)] / (High - Low)</c>,
/// <c>MFV = MFM × Volume</c>, <c>CMF = Sum(MFV, period) / Sum(Volume, period)</c>.
/// </remarks>
/// <seealso href="Cmf.md">Detailed documentation</seealso>
/// <seealso href="cmf.pine">Reference Pine Script implementation</seealso>
[SkipLocalsInit]
public sealed class Cmf : ITValuePublisher
{
private readonly int _period;
private readonly RingBuffer _mfvBuffer;
private readonly RingBuffer _volBuffer;
private double _sumMfv;
private double _sumVol;
private double _p_sumMfv;
private double _p_sumVol;
private int _index;
private int _p_index;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current CMF value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the indicator has processed enough bars (period).
/// </summary>
public bool IsHot => _index >= _period;
/// <summary>
/// Warmup period required before the indicator is considered hot.
/// </summary>
public int WarmupPeriod => _period;
/// <summary>
/// Creates a new CMF indicator.
/// </summary>
/// <param name="period">Lookback period (default: 20)</param>
/// <exception cref="ArgumentException">Thrown when period is less than 1.</exception>
public Cmf(int period = 20)
{
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
_period = period;
_mfvBuffer = new RingBuffer(period);
_volBuffer = new RingBuffer(period);
Name = $"CMF({period})";
}
/// <summary>
/// Resets the indicator state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_mfvBuffer.Clear();
_volBuffer.Clear();
_sumMfv = 0;
_sumVol = 0;
_p_sumMfv = 0;
_p_sumVol = 0;
_index = 0;
_p_index = 0;
Last = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_sumMfv = _sumMfv;
_p_sumVol = _sumVol;
_p_index = _index;
_mfvBuffer.Snapshot();
_volBuffer.Snapshot();
}
else
{
_sumMfv = _p_sumMfv;
_sumVol = _p_sumVol;
_index = _p_index;
_mfvBuffer.Restore();
_volBuffer.Restore();
}
double highLowRange = input.High - input.Low;
double mfm = 0;
if (highLowRange > double.Epsilon)
{
mfm = (input.Close - input.Low - (input.High - input.Close)) / highLowRange;
}
double mfv = mfm * input.Volume;
double vol = input.Volume;
// Update rolling sums
if (_mfvBuffer.IsFull)
{
_sumMfv -= _mfvBuffer.Oldest;
_sumVol -= _volBuffer.Oldest;
}
_mfvBuffer.Add(mfv);
_volBuffer.Add(vol);
_sumMfv += mfv;
_sumVol += vol;
if (isNew)
{
_index++;
}
// Calculate CMF
double cmfValue = _sumVol > double.Epsilon ? _sumMfv / _sumVol : 0;
Last = new TValue(input.Time, cmfValue);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
/// <summary>
/// Updates CMF with a TValue input.
/// </summary>
/// <exception cref="NotSupportedException">
/// CMF requires OHLCV bar data to calculate the Money Flow Multiplier and Volume.
/// Use Update(TBar) instead.
/// </exception>
#pragma warning disable S2325 // Method signature must match ITValuePublisher contract
public TValue Update(TValue input, bool isNew = true)
#pragma warning restore S2325
{
throw new NotSupportedException(
"CMF requires OHLCV bar data to calculate the Money Flow Multiplier and Volume. " +
"Use Update(TBar) instead.");
}
public TSeries Update(TBarSeries source)
{
var t = new List<long>(source.Count);
var v = new List<double>(source.Count);
Reset();
for (int i = 0; i < source.Count; i++)
{
var val = Update(source[i], isNew: true);
t.Add(val.Time);
v.Add(val.Value);
}
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
/// <param name="source">Historical bar data.</param>
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source, int period = 20)
{
if (source.Count == 0)
{
return [];
}
var t = source.Open.Times.ToArray();
var v = new double[source.Count];
Batch(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v, period);
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, ReadOnlySpan<double> volume, Span<double> output, int period = 20)
{
if (high.Length != low.Length)
{
throw new ArgumentException("High and Low spans must be of the same length", nameof(low));
}
if (high.Length != close.Length)
{
throw new ArgumentException("High and Close spans must be of the same length", nameof(close));
}
if (high.Length != volume.Length)
{
throw new ArgumentException("High and Volume spans must be of the same length", nameof(volume));
}
if (high.Length != output.Length)
{
throw new ArgumentException("Output span must be of the same length as input", nameof(output));
}
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
int len = high.Length;
// First, compute MFV for each bar
Span<double> mfv = len <= 512 ? stackalloc double[len] : new double[len];
int i = 0;
if (Vector.IsHardwareAccelerated && len >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;
var epsilon = new Vector<double>(double.Epsilon);
for (; i <= len - vectorSize; i += vectorSize)
{
var h = new Vector<double>(high.Slice(i, vectorSize));
var l = new Vector<double>(low.Slice(i, vectorSize));
var c = new Vector<double>(close.Slice(i, vectorSize));
var vol = new Vector<double>(volume.Slice(i, vectorSize));
var hl = h - l;
var num = c - l - (h - c);
var mask = Vector.GreaterThan(hl, epsilon);
var safeHl = Vector.ConditionalSelect(mask, hl, Vector<double>.One);
var mfm = num / safeHl;
mfm = Vector.ConditionalSelect(mask, mfm, Vector<double>.Zero);
var result = mfm * vol;
result.CopyTo(mfv.Slice(i, vectorSize));
}
}
for (; i < len; i++)
{
double h = high[i];
double l = low[i];
double c = close[i];
double vol = volume[i];
double hl = h - l;
double mfm = 0;
if (hl > double.Epsilon)
{
mfm = (c - l - (h - c)) / hl;
}
mfv[i] = mfm * vol;
}
// Now compute CMF using rolling sums
double sumMfv = 0;
double sumVol = 0;
for (i = 0; i < len; i++)
{
sumMfv += mfv[i];
sumVol += volume[i];
if (i >= period)
{
sumMfv -= mfv[i - period];
sumVol -= volume[i - period];
}
output[i] = sumVol > double.Epsilon ? sumMfv / sumVol : 0;
}
}
public static (TSeries Results, Cmf Indicator) Calculate(TBarSeries source, int period = 20)
{
var indicator = new Cmf(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
}