using System.Runtime.CompilerServices; using System.Numerics; namespace QuanTAlib; /// /// CMF: Chaikin Money Flow /// /// /// Measures buying/selling pressure via close position within range and volume. /// Oscillates between -1 and +1; positive indicates accumulation, negative indicates distribution. /// /// Calculation: MFM = [(Close - Low) - (High - Close)] / (High - Low), /// MFV = MFM × Volume, CMF = Sum(MFV, period) / Sum(Volume, period). /// /// Detailed documentation /// Reference Pine Script implementation [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; /// /// Display name for the indicator. /// public string Name { get; } public event TValuePublishedHandler? Pub; /// /// Current CMF value. /// public TValue Last { get; private set; } /// /// True if the indicator has processed enough bars (period). /// public bool IsHot => _index >= _period; /// /// Warmup period required before the indicator is considered hot. /// public int WarmupPeriod => _period; /// /// Creates a new CMF indicator. /// /// Lookback period (default: 20) /// Thrown when period is less than 1. 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})"; } /// /// Resets the indicator state. /// [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; } /// /// Updates CMF with a TValue input. /// /// /// CMF requires OHLCV bar data to calculate the Money Flow Multiplier and Volume. /// Use Update(TBar) instead. /// #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(source.Count); var v = new List(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); } /// /// Initializes the indicator state using the provided bar series history. /// /// Historical bar data. 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 high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span 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 mfv = len <= 512 ? stackalloc double[len] : new double[len]; int i = 0; if (Vector.IsHardwareAccelerated && len >= Vector.Count) { int vectorSize = Vector.Count; var epsilon = new Vector(double.Epsilon); for (; i <= len - vectorSize; i += vectorSize) { var h = new Vector(high.Slice(i, vectorSize)); var l = new Vector(low.Slice(i, vectorSize)); var c = new Vector(close.Slice(i, vectorSize)); var vol = new Vector(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.One); var mfm = num / safeHl; mfm = Vector.ConditionalSelect(mask, mfm, Vector.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); } }