using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// MIDPRICE: Midpoint Price over Period /// Calculates the midpoint of the highest High and lowest Low over a rolling window. /// Unlike Midpoint (which operates on a single series), Midprice uses separate H/L channels. /// /// /// Calculation: /// /// MidPrice = (Highest(High, N) + Lowest(Low, N)) / 2 /// /// /// Key characteristics: /// /// Rolling bar-level calculation with lookback period /// TA-Lib compatible (MIDPRICE function) /// Uses RingBuffer directly for self-contained core dependency /// Represents the center of the price channel over the lookback window /// /// /// Difference from Midpoint: /// /// Midpoint operates on a single value series: (Highest(V,N) + Lowest(V,N)) / 2 /// Midprice operates on OHLC bars: (Highest(H,N) + Lowest(L,N)) / 2 /// /// [SkipLocalsInit] public sealed class Midprice : AbstractBase { private readonly int _period; private readonly RingBuffer _highBuffer; private readonly RingBuffer _lowBuffer; [StructLayout(LayoutKind.Auto)] private record struct State(double LastValidHigh, double LastValidLow); private State _s, _ps; /// /// True if both internal buffers have enough data for valid results. /// public override bool IsHot => _highBuffer.Count >= _period; /// /// Initializes a new instance of the Midprice class. /// /// Lookback window size (must be >= 1) public Midprice(int period) { if (period < 1) { throw new ArgumentException("Period must be >= 1", nameof(period)); } _period = period; _highBuffer = new RingBuffer(period); _lowBuffer = new RingBuffer(period); Name = $"Midprice({period})"; WarmupPeriod = period; } /// /// Initializes a new instance of the Midprice class with a source. /// /// The data source for chaining. /// Lookback window size. public Midprice(ITValuePublisher source, int period) : this(period) { source.Pub += Handle; } private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); /// /// Updates the indicator with a TValue input. /// For TValue input, treats the value as both High and Low (same as Midpoint behavior). /// Prefer Update(TBar) for standard OHLC data. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { return UpdateCore(input.Time, input.Value, input.Value, isNew); } /// /// Updates the indicator with a new bar (preferred method). /// /// The input bar. /// Whether this is a new bar or an update. /// The calculated Midprice value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar bar, bool isNew = true) { return UpdateCore(bar.Time, bar.High, bar.Low, isNew); } /// /// Updates the indicator with a bar series. /// /// The source bar series. /// A TSeries containing the Midprice values. public TSeries Update(TBarSeries source) { if (source.Count == 0) { return []; } int len = source.Count; var t = new List(len); var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); Batch(source.HighValues, source.LowValues, vSpan, WarmupPeriod); for (int i = 0; i < len; i++) { tSpan[i] = source[i].Time; } // Update internal state for (int i = 0; i < len; i++) { Update(source[i], isNew: true); } return new TSeries(t, v); } 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; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private TValue UpdateCore(long timeTicks, double high, double low, bool isNew) { if (isNew) { _ps = _s; } else { _s = _ps; } var s = _s; double h = double.IsFinite(high) ? high : s.LastValidHigh; double l = double.IsFinite(low) ? low : s.LastValidLow; s = new State(h, l); _highBuffer.Add(h, isNew); _lowBuffer.Add(l, isNew); double result = (_highBuffer.Max() + _lowBuffer.Min()) * 0.5; _s = s; Last = new TValue(timeTicks, result); PubEvent(Last, isNew); return Last; } 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 override void Reset() { _highBuffer.Clear(); _lowBuffer.Clear(); _s = default; _ps = default; Last = default; } /// /// Calculates Midprice for a bar series (static). /// public static TSeries Batch(TBarSeries source, int period) { var indicator = new Midprice(period); return indicator.Update(source); } /// /// Batch calculation using spans for High/Low data with rolling window. /// public static void Batch( ReadOnlySpan high, ReadOnlySpan low, Span output, int period) { int len = high.Length; if (low.Length != len) { throw new ArgumentException("High and Low spans must have the same length", nameof(low)); } if (output.Length < len) { throw new ArgumentException("Output span must be at least as long as input spans", nameof(output)); } if (period < 1) { throw new ArgumentException("Period must be >= 1", nameof(period)); } // Use RingBuffer for rolling max/min — self-contained, no Highest/Lowest dependency var highBuf = new RingBuffer(period); var lowBuf = new RingBuffer(period); for (int i = 0; i < len; i++) { double fallback = i > 0 ? output[i - 1] : 0; double h = double.IsFinite(high[i]) ? high[i] : fallback; double l = double.IsFinite(low[i]) ? low[i] : fallback; highBuf.Add(h, true); lowBuf.Add(l, true); output[i] = (highBuf.Max() + lowBuf.Min()) * 0.5; } } /// /// Batch calculation using a TBarSeries (convenience overload). /// public static void Batch(TBarSeries source, Span output, int period) { int len = source.Count; if (output.Length < len) { throw new ArgumentException("Output span must be at least as long as source", nameof(output)); } if (len == 0) { return; } Batch(source.HighValues, source.LowValues, output, period); } public static (TSeries Results, Midprice Indicator) Calculate(TBarSeries source, int period) { var indicator = new Midprice(period); TSeries results = indicator.Update(source); return (results, indicator); } }