// MIDPOINT: Rolling Midpoint - (Highest + Lowest) / 2 over lookback window // Composes Highest and Lowest indicators for efficient calculation using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// MIDPOINT: Rolling Midpoint /// Calculates the midpoint ((highest + lowest) / 2) over a specified lookback period. /// Composes Highest and Lowest indicators internally. /// /// /// Key properties: /// - Returns the center of the price range within the lookback window /// - Useful for mean reversion, channel center, trend direction /// - Can be validated against TA-Lib MIDPOINT function /// [SkipLocalsInit] public sealed class Midpoint : AbstractBase { private readonly Highest _highest; private readonly Lowest _lowest; private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _handler; private bool _disposed; public override bool IsHot => _highest.IsHot && _lowest.IsHot; /// /// Initializes a new Midpoint indicator with specified lookback period. /// /// Lookback window size (must be >= 1) public Midpoint(int period) { if (period < 1) { throw new ArgumentException("Period must be >= 1", nameof(period)); } _highest = new Highest(period); _lowest = new Lowest(period); Name = $"Midpoint({period})"; WarmupPeriod = period; } /// /// Initializes a new Midpoint indicator with source for event-based chaining. /// /// Source indicator for chaining /// Lookback window size public Midpoint(ITValuePublisher source, int period) : this(period) { _source = source; _handler = HandleUpdate; _source.Pub += _handler; } protected override void Dispose(bool disposing) { if (!_disposed) { if (disposing && _source != null && _handler != null) { _source.Pub -= _handler; } _disposed = true; } base.Dispose(disposing); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { TValue high = _highest.Update(input, isNew); TValue low = _lowest.Update(input, isNew); double result = (high.Value + low.Value) * 0.5; Last = new TValue(input.Time, result); PubEvent(Last, isNew); return Last; } public override TSeries Update(TSeries source) { var result = new TSeries(source.Count); ReadOnlySpan values = source.Values; ReadOnlySpan times = source.Times; for (int i = 0; i < source.Count; i++) { var tv = Update(new TValue(new DateTime(times[i], DateTimeKind.Utc), values[i]), true); result.Add(tv, true); } return result; } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { TimeSpan interval = step ?? TimeSpan.FromSeconds(1); DateTime time = DateTime.UtcNow - (interval * source.Length); for (int i = 0; i < source.Length; i++) { Update(new TValue(time, source[i]), true); time += interval; } } public static TSeries Batch(TSeries source, int period) { var indicator = new Midpoint(period); return indicator.Update(source); } /// /// Calculates rolling midpoint over a span of values. /// public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length == 0) { throw new ArgumentException("Source cannot be empty", nameof(source)); } if (output.Length < source.Length) { throw new ArgumentException("Output length must be >= source length", nameof(output)); } if (period < 1) { throw new ArgumentException("Period must be >= 1", nameof(period)); } int len = source.Length; // Use ArrayPool for large arrays to avoid stack overflow double[]? rentedHigh = null; double[]? rentedLow = null; #pragma warning disable S1121 // Assignments should not be made from within sub-expressions Span highBuffer = len <= 256 ? stackalloc double[len] : (rentedHigh = System.Buffers.ArrayPool.Shared.Rent(len)).AsSpan(0, len); Span lowBuffer = len <= 256 ? stackalloc double[len] : (rentedLow = System.Buffers.ArrayPool.Shared.Rent(len)).AsSpan(0, len); #pragma warning restore S1121 try { Highest.Batch(source, highBuffer, period); Lowest.Batch(source, lowBuffer, period); for (int i = 0; i < len; i++) { output[i] = (highBuffer[i] + lowBuffer[i]) * 0.5; } } finally { if (rentedHigh != null) { System.Buffers.ArrayPool.Shared.Return(rentedHigh); } if (rentedLow != null) { System.Buffers.ArrayPool.Shared.Return(rentedLow); } } } public static (TSeries Results, Midpoint Indicator) Calculate(TSeries source, int period) { var indicator = new Midpoint(period); TSeries results = indicator.Update(source); return (results, indicator); } public override void Reset() { _highest.Reset(); _lowest.Reset(); Last = default; } }