using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Numerics; namespace QuanTAlib; /// /// BOP: Balance of Power /// /// /// Buyer/seller strength oscillator: (Close-Open)/(High-Low). /// Ranges [-1,1]: positive = buyers dominate, negative = sellers dominate. /// /// Calculation: BOP = (Close - Open) / (High - Low). /// /// Detailed documentation [SkipLocalsInit] public sealed class Bop : ITValuePublisher { /// /// Display name for the indicator. /// public static string Name => "Bop"; public event TValuePublishedHandler? Pub; /// /// Current BOP value. /// public TValue Last { get; private set; } /// /// True if the indicator has a valid value (always true for BOP as it has no warmup). /// public static bool IsHot => true; /// /// The number of bars required for the indicator to warm up. /// public static int WarmupPeriod => 0; /// /// Resets the indicator state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { Last = default; } /// /// Updates the indicator with a new bar. /// /// The input bar. /// Whether this is a new bar or an update to the current one. /// The updated BOP value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { double range = input.High - input.Low; double bop = 0; if (range > double.Epsilon) { bop = (input.Close - input.Open) / range; } Last = new TValue(input.Time, bop); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates the indicator with a new value (not supported for BOP as it requires OHLC). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { // BOP requires OHLC, so we can't calculate it from a single value. // We'll treat the input value as Close, and assume Open=Close, High=Close, Low=Close, // which results in 0/0 -> 0. // Or we could throw NotSupportedException. // Given the interface contract, returning 0 is safer than crashing. Last = new TValue(input.Time, 0); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates the indicator with a series of bars. /// public static TSeries Update(TBarSeries source) { return Batch(source); } /// /// Calculates BOP for a series of bars. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan open, ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, Span destination) { int len = Math.Min(open.Length, Math.Min(high.Length, Math.Min(low.Length, close.Length))); if (destination.Length < len) { len = destination.Length; } int i = 0; if (Vector.IsHardwareAccelerated && len >= Vector.Count) { var epsilon = new Vector(double.Epsilon); ref var oRef = ref MemoryMarshal.GetReference(open); ref var hRef = ref MemoryMarshal.GetReference(high); ref var lRef = ref MemoryMarshal.GetReference(low); ref var cRef = ref MemoryMarshal.GetReference(close); ref var dRef = ref MemoryMarshal.GetReference(destination); while (i <= len - Vector.Count) { var o = Vector.LoadUnsafe(ref oRef, (nuint)i); var h = Vector.LoadUnsafe(ref hRef, (nuint)i); var l = Vector.LoadUnsafe(ref lRef, (nuint)i); var c = Vector.LoadUnsafe(ref cRef, (nuint)i); var range = h - l; var body = c - o; // Create a mask where range > Epsilon var mask = Vector.GreaterThan(range, epsilon); // Perform division (results in NaN/Inf if range is 0, but we'll mask it out) var div = body / range; // Select div where mask is true, otherwise 0 var result = Vector.ConditionalSelect(mask, div, Vector.Zero); result.StoreUnsafe(ref dRef, (nuint)i); i += Vector.Count; } } for (; i < len; i++) { double range = high[i] - low[i]; destination[i] = range > double.Epsilon ? (close[i] - open[i]) / range : 0; } } /// /// Calculates BOP for a TBarSeries. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSeries Batch(TBarSeries source) { if (source.Count == 0) { return new TSeries([], []); } var 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); source.Open.Times.CopyTo(tSpan); Batch(source.Open.Values, source.High.Values, source.Low.Values, source.Close.Values, vSpan); return new TSeries(t, v); } /// /// Initializes the indicator state using the provided bar series history. /// /// Historical bar data. public void Prime(TBarSeries source) { Reset(); if (source.Count == 0) { return; } for (int i = 0; i < source.Count; i++) { Update(source[i], isNew: true); } } public static (TSeries Results, Bop Indicator) Calculate(TBarSeries source) { var indicator = new Bop(); TSeries results = Bop.Update(source); return (results, indicator); } }