// IMI: Intraday Momentum Index // Developed by Tushar Chande // Combines candlestick analysis with RSI-like calculation // Uses gain/loss based on intraday Open-Close relationship using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// IMI: Intraday Momentum Index /// /// /// A technical indicator developed by Tushar Chande that combines candlestick analysis /// with RSI-like overbought/oversold signals. Unlike RSI which uses close-to-close changes, /// IMI uses the relationship between each bar's open and close prices. /// /// Calculation: /// Gain = Close - Open (when Close > Open, otherwise 0) /// Loss = Open - Close (when Close < Open, otherwise 0) /// IMI = 100 × Sum(Gains, n) / (Sum(Gains, n) + Sum(Losses, n)) /// /// Key Levels: /// - Above 70: Overbought condition /// - Below 30: Oversold condition /// - 50: Neutral (equal up and down momentum) /// /// Sources: /// - Investopedia: https://www.investopedia.com/terms/i/intraday-momentum-index-imi.asp /// - CQG: https://help.cqg.com/cqgic/25/Documents/intradaymomentumindeximi.htm /// [SkipLocalsInit] public sealed class Imi : ITValuePublisher { private readonly int _period; private readonly RingBuffer _gains; private readonly RingBuffer _losses; // Rolling sums for O(1) updates private double _gainSum; private double _lossSum; // Bar correction state private double _savedGainSum; private double _savedLossSum; /// /// Display name for the indicator. /// public string Name { get; } /// /// Event publisher for value updates. /// public event TValuePublishedHandler? Pub; /// /// Current IMI value. /// public TValue Last { get; private set; } /// /// True if the indicator has enough data for a full period calculation. /// public bool IsHot => _gains.IsFull; /// /// The period parameter. /// public int Period => _period; /// /// The number of bars required for the indicator to warm up. /// public int WarmupPeriod { get; } /// /// Creates IMI indicator with specified period. /// /// Lookback period (must be >= 1) public Imi(int period = 14) { if (period < 1) { throw new ArgumentException("Period must be at least 1", nameof(period)); } _period = period; Name = $"IMI({period})"; WarmupPeriod = period; _gains = new RingBuffer(period); _losses = new RingBuffer(period); _gainSum = 0.0; _lossSum = 0.0; _savedGainSum = 0.0; _savedLossSum = 0.0; } /// /// Resets the indicator state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _gains.Clear(); _losses.Clear(); _gainSum = 0.0; _lossSum = 0.0; _savedGainSum = 0.0; _savedLossSum = 0.0; Last = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void PubEvent(TValue value, bool isNew = true) => Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew }); /// /// Updates the IMI indicator with a new bar. /// /// The price bar (Open, Close required) /// True for new bar, false for update of current bar /// The current IMI value [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { double open = input.Open; double close = input.Close; // Handle NaN/Infinity inputs if (!double.IsFinite(open) || !double.IsFinite(close)) { PubEvent(Last, isNew); return Last; } if (isNew) { // Save state for potential correction _savedGainSum = _gainSum; _savedLossSum = _lossSum; } else { // Restore state for correction _gainSum = _savedGainSum; _lossSum = _savedLossSum; } // Calculate gain and loss for this bar double gain = 0.0; double loss = 0.0; if (close > open) { gain = close - open; } else if (close < open) { loss = open - close; } // When close == open, both gain and loss remain 0 // Update rolling sums: subtract old value if buffer is full if (_gains.IsFull) { _gainSum -= _gains[0]; _lossSum -= _losses[0]; } // Add new values to buffers _gains.Add(gain, isNew); _losses.Add(loss, isNew); _gainSum += gain; _lossSum += loss; // Calculate IMI double total = _gainSum + _lossSum; double imi = total > 0 ? 100.0 * _gainSum / total : 50.0; Last = new TValue(input.Time, imi); PubEvent(Last, isNew); return Last; } /// /// Calculates IMI for the entire bar series. /// public TSeries Update(TBarSeries source) { if (source.Count == 0) { return new TSeries([], []); } int len = source.Count; var tList = new List(len); var vList = new List(len); for (int i = 0; i < len; i++) { var bar = source[i]; Update(bar, isNew: true); tList.Add(bar.Time); vList.Add(Last.Value); } return new TSeries(tList, vList); } /// /// Primes the indicator with historical bar data. /// public void Prime(TBarSeries source) { for (int i = 0; i < source.Count; i++) { Update(source[i], isNew: true); } } /// /// Calculates IMI for the entire bar series using default parameters. /// public static TSeries Batch(TBarSeries source) { var imi = new Imi(); return imi.Update(source); } /// /// Calculates IMI for the entire bar series using custom period. /// public static TSeries Batch(TBarSeries source, int period) { var imi = new Imi(period); return imi.Update(source); } /// /// Calculates IMI and returns both results and the warm indicator. /// public static (TSeries Results, Imi Indicator) Calculate(TBarSeries source, int period = 14) { var imi = new Imi(period); var results = imi.Update(source); return (results, imi); } }