using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// Computes the Bill Williams Market Facilitation Index (BW_MFI) with 4-zone classification, /// measuring price movement efficiency per unit of volume and categorizing each bar into /// one of four market states based on MFI and volume direction changes. /// /// /// BW_MFI Formula: /// MFI = (High − Low) / Volume, /// Zone classification by comparing current vs previous bar: /// Zone 1 (Green): MFI↑ + Volume↑ → trend continuation, /// Zone 2 (Fade): MFI↓ + Volume↓ → fading momentum, /// Zone 3 (Fake): MFI↑ + Volume↓ → fake breakout, /// Zone 4 (Squat): MFI↓ + Volume↑ → accumulation/distribution. /// /// Zone 4 (Squat) is the most significant: large volume with small range indicates a /// battle between bulls and bears, often preceding a breakout. Zone 1 (Green) confirms /// trend strength. Zone 3 (Fake) warns of unsupported price moves. /// This implementation is optimized for streaming updates with O(1) per bar. /// Non-finite inputs (NaN/±Inf) are sanitized by substituting the last finite value observed. /// /// For the authoritative algorithm reference, full rationale, and behavioral contracts, see the /// companion files in the same directory. /// /// Detailed documentation /// Reference Pine Script implementation [SkipLocalsInit] public sealed class BwMfi : ITValuePublisher { [StructLayout(LayoutKind.Auto)] private record struct State( double LastValid, double PrevMfi, double PrevVolume, int Count); private State _s; private State _ps; private readonly TBarPublishedHandler _barHandler; /// Display name for the indicator. public string Name { get; } /// Bars required for the first valid zone output (2 — need previous bar for comparison). public static int WarmupPeriod => 2; /// True when at least two bars have been processed (zone classification requires comparison). public bool IsHot => _s.Count >= 2; /// Current BW_MFI value (price range per unit of volume). public TValue Last { get; private set; } /// Current zone classification (1=Green, 2=Fade, 3=Fake, 4=Squat, 0=insufficient data). public int Zone { get; private set; } public event TValuePublishedHandler? Pub; /// Creates a BW_MFI indicator. public BwMfi() { _s = new State(0.0, 0.0, 0.0, 0); _ps = _s; Name = "BwMfi"; _barHandler = HandleBar; } /// Creates BW_MFI chained to a TBarSeries source. public BwMfi(TBarSeries source) : this() { Prime(source); source.Pub += _barHandler; } private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] private void PubEvent(TValue value, bool isNew) => Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew }); /// Resets all state to initial conditions. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _s = new State(0.0, 0.0, 0.0, 0); _ps = _s; Last = default; Zone = 0; } /// /// Updates BW_MFI with a new OHLCV bar. /// /// OHLCV bar data /// True to advance state; false to rewrite the latest bar /// Current BW_MFI value as TValue [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { var s = _s; if (isNew) { _ps = s; s.Count++; } else { int count = s.Count; s = _ps; s.Count = count; } // Sanitize OHLCV inputs — use last-valid on NaN/Infinity double high = double.IsFinite(input.High) ? input.High : s.LastValid; double low = double.IsFinite(input.Low) ? input.Low : s.LastValid; double volume = double.IsFinite(input.Volume) ? input.Volume : 0.0; // Core formula: price range per unit of volume double mfi = volume != 0.0 ? (high - low) / volume : 0.0; if (double.IsFinite(mfi)) { s.LastValid = mfi; } else { mfi = s.LastValid; } // Zone classification: requires previous bar comparison int zone; if (s.Count < 2) { zone = 0; // insufficient data } else { bool mfiUp = mfi > s.PrevMfi; bool volUp = volume > s.PrevVolume; if (mfiUp && volUp) { zone = 1; // Green: trend continuation } else if (!mfiUp && !volUp) { zone = 2; // Fade: fading momentum } else if (mfiUp && !volUp) { zone = 3; // Fake: unsupported price move } else { zone = 4; // Squat: accumulation/distribution } } // Store current values for next comparison s.PrevMfi = mfi; s.PrevVolume = volume; _s = s; Zone = zone; Last = new TValue(input.Time, mfi); PubEvent(Last, isNew); return Last; } /// /// Updates BW_MFI from a scalar TValue (uses Val as proxy; High=Low=Val, Volume=1). /// Primarily for ITValuePublisher compatibility — TBar is the natural input for BW_MFI. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { double v = double.IsFinite(input.Value) ? input.Value : _s.LastValid; return Update(new TBar(input.Time, v, v, v, v, 1.0), isNew); } /// /// Batch-computes BW_MFI and zones over raw High/Low/Volume spans. Zero-allocation path. /// /// Source high prices /// Source low prices /// Source volume /// Destination span for MFI values /// Destination span for zone classifications (1-4, 0 for first bar) public static void Batch( ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan volume, Span mfiOutput, Span zoneOutput) { int len = high.Length; if (low.Length != len) { throw new ArgumentException("Low length must match high length", nameof(low)); } if (volume.Length != len) { throw new ArgumentException("Volume length must match high length", nameof(volume)); } if (mfiOutput.Length != len) { throw new ArgumentException("MFI output length must match input length", nameof(mfiOutput)); } if (zoneOutput.Length != len) { throw new ArgumentException("Zone output length must match input length", nameof(zoneOutput)); } if (len == 0) { return; } // First bar: compute MFI, zone = 0 (no previous to compare) double v0 = double.IsFinite(volume[0]) ? volume[0] : 0.0; double mfi0 = v0 != 0.0 ? (high[0] - low[0]) / v0 : 0.0; mfiOutput[0] = mfi0; zoneOutput[0] = 0; double prevMfi = mfi0; double prevVol = v0; for (int i = 1; i < len; i++) { double h = high[i]; double l = low[i]; double vol = double.IsFinite(volume[i]) ? volume[i] : 0.0; double mfi = vol != 0.0 ? (h - l) / vol : 0.0; mfiOutput[i] = mfi; bool mfiUp = mfi > prevMfi; bool volUp = vol > prevVol; if (mfiUp && volUp) { zoneOutput[i] = 1; } else if (!mfiUp && !volUp) { zoneOutput[i] = 2; } else if (mfiUp && !volUp) { zoneOutput[i] = 3; } else { zoneOutput[i] = 4; } prevMfi = mfi; prevVol = vol; } } /// /// Batch-computes BW_MFI values only (without zones) over raw spans. Zero-allocation path. /// public static void Batch( ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan volume, Span output) { int len = high.Length; if (low.Length != len) { throw new ArgumentException("Low length must match high length", nameof(low)); } if (volume.Length != len) { throw new ArgumentException("Volume length must match high length", nameof(volume)); } if (output.Length != len) { throw new ArgumentException("Output length must match input length", nameof(output)); } for (int i = 0; i < len; i++) { double h = high[i]; double l = low[i]; double v = double.IsFinite(volume[i]) ? volume[i] : 0.0; output[i] = v != 0.0 ? (h - l) / v : 0.0; } } /// Primes the indicator by replaying historical data without firing events. public void Prime(TBarSeries source) { foreach (var bar in source) { Update(bar, isNew: true); } } }