using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// MIDBODY: Open-Close Average /// Calculates the midpoint of Open and Close prices. /// Equivalent to TBar.OC2 but as a proper streaming indicator with bar correction. /// /// /// Calculation: /// /// Midbody = (Open + Close) / 2 /// /// /// Key characteristics: /// /// Stateless bar-by-bar calculation (no lookback period) /// Skender compatible (CandlePart.OC2) /// Always hot after first bar /// Captures the midpoint between session open and close /// /// [SkipLocalsInit] public sealed class Midbody : AbstractBase { [StructLayout(LayoutKind.Auto)] private record struct State( double LastValidOpen, double LastValidClose, double LastResult, int Count ); private State _s; private State _ps; /// /// Initializes a new instance of the Midbody class. /// public Midbody() { WarmupPeriod = 1; Name = "Midbody"; _s = new State(0, 0, 0, 0); _ps = _s; } /// /// Initializes a new instance of the Midbody class with a source. /// /// The data source for chaining. public Midbody(ITValuePublisher source) : this() { source.Pub += Handle; } private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); /// /// True if the indicator has enough data for valid results. /// public override bool IsHot => _s.Count >= WarmupPeriod; /// /// Computes the Midbody price from Open and Close values. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double ComputeMidbody(double open, double close) { return (open + close) * 0.5; } /// /// Updates the indicator with a TValue input. /// For TValue input, treats the value as both Open and Close (result = value). /// 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 Midbody value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar bar, bool isNew = true) { return UpdateCore(bar.Time, bar.Open, bar.Close, isNew); } /// /// Updates the indicator with a bar series. /// /// The source bar series. /// A TSeries containing the Midbody 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.OpenValues, source.CloseValues, vSpan); 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) { 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); var values = source.Values; // TValue-only: result = value (identity) for (int i = 0; i < len; i++) { tSpan[i] = source.Times[i]; vSpan[i] = values[i]; } // Update internal state for (int i = 0; i < len; i++) { Update(new TValue(source.Times[i], values[i]), isNew: true); } return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private TValue UpdateCore(long timeTicks, double open, double close, bool isNew) { if (isNew) { _ps = _s; } else { _s = _ps; } var s = _s; // Handle non-finite values — use last valid values if (!double.IsFinite(open)) { open = s.LastValidOpen; } else { s.LastValidOpen = open; } if (!double.IsFinite(close)) { close = s.LastValidClose; } else { s.LastValidClose = close; } double result = ComputeMidbody(open, close); if (!double.IsFinite(result)) { result = s.LastResult; } else { s.LastResult = result; } if (isNew) { s.Count++; } _s = s; Last = new TValue(timeTicks, result); PubEvent(Last, isNew); return Last; } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { for (int i = 0; i < source.Length; i++) { Update(new TValue(DateTime.UtcNow, source[i]), isNew: true); } } public override void Reset() { _s = new State(0, 0, 0, 0); _ps = _s; Last = default; } /// /// Calculates Midbody for a bar series (static). /// public static TSeries Batch(TBarSeries source) { var indicator = new Midbody(); return indicator.Update(source); } /// /// Batch calculation using spans for Open/Close data. /// public static void Batch( ReadOnlySpan open, ReadOnlySpan close, Span output) { int len = open.Length; if (close.Length != len) { throw new ArgumentException("All input spans must have the same length", nameof(close)); } if (output.Length < len) { throw new ArgumentException("Output span must be at least as long as input spans", nameof(output)); } for (int i = 0; i < len; i++) { output[i] = ComputeMidbody(open[i], close[i]); } } /// /// Batch calculation using a TBarSeries (convenience overload). /// public static void Batch(TBarSeries source, Span output) { 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.OpenValues, source.CloseValues, output); } public static (TSeries Results, Midbody Indicator) Calculate(TBarSeries source) { var indicator = new Midbody(); TSeries results = indicator.Update(source); return (results, indicator); } }