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);
}
}