// MARKETFI: Market Facilitation Index // Bill Williams' measure of price movement efficiency per unit of volume. // Formula: MFI = (High - Low) / Volume // Guard: Volume == 0 → 0.0 (no market activity = zero facilitation) using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// MARKETFI: Market Facilitation Index /// /// /// Bill Williams' efficiency measure: how much price moves per unit of volume. /// /// MARKETFI = (High − Low) / Volume /// Zero volume guard: result = 0.0 (market closed / no activity) /// Large MARKETFI + rising volume → strong trend continuation /// Small MARKETFI + falling volume → squat / accumulation phase /// /// O(1) per bar — no period, no buffers, pure division. /// IsHot fires on the first bar (no warmup required). /// /// References: /// Williams, Bill (1995). Trading Chaos. /// PineScript reference: marketfi.pine /// [SkipLocalsInit] public sealed class Marketfi : ITValuePublisher { [StructLayout(LayoutKind.Auto)] private record struct State( double LastValid, 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 output (always 1 — no warmup). public static int WarmupPeriod => 1; /// True from the first bar onward. public bool IsHot => _s.Count >= 1; /// Current MARKETFI value (price range per unit of volume). public TValue Last { get; private set; } public event TValuePublishedHandler? Pub; /// Creates a MARKETFI indicator. public Marketfi() { _s = new State(0.0, 0); _ps = _s; Name = "Marketfi"; _barHandler = HandleBar; } /// Creates MARKETFI chained to a TBarSeries source. public Marketfi(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); _ps = _s; Last = default; } /// /// Updates MARKETFI with a new OHLCV bar. /// /// OHLCV bar data /// True to advance state; false to rewrite the latest bar /// Current MARKETFI value as TValue [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { var s = _s; if (isNew) { _ps = s; s.Count++; } else { s = _ps; } // 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 // Zero-volume guard: return 0.0 (no facilitation when no trades occurred) double mfi = volume != 0.0 ? (high - low) / volume : 0.0; if (double.IsFinite(mfi)) { s.LastValid = mfi; } else { mfi = s.LastValid; } _s = s; Last = new TValue(input.Time, mfi); PubEvent(Last, isNew); return Last; } /// /// Updates MARKETFI from a scalar TValue (uses Val as proxy; High=Low=Val, Volume=1). /// Primarily for ITValuePublisher compatibility — TBar is the natural input for MARKETFI. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { double v = double.IsFinite(input.Value) ? input.Value : _s.LastValid; // Scalar input: treat as zero-range bar with unit volume → MFI = 0 return Update(new TBar(input.Time, v, v, v, v, 1.0), isNew); } /// /// Batch-computes MARKETFI over raw High/Low/Volume spans. Zero-allocation path. /// /// Source high prices /// Source low prices /// Source volume /// Destination span for MARKETFI values 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); } } }