// 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);
}
}
}