Files
QuanTAlib/lib/momentum/bop/Bop.cs
T

177 lines
5.5 KiB
C#

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Numerics;
namespace QuanTAlib;
/// <summary>
/// BOP: Balance of Power
/// </summary>
/// <remarks>
/// BOP measures the strength of buyers vs sellers by comparing the close price to the open price,
/// relative to the high-low range.
///
/// Formula:
/// BOP = (Close - Open) / (High - Low)
///
/// Key characteristics:
/// - Oscillates between -1 and 1
/// - 1 indicates buyers dominated (Close = High, Open = Low)
/// - -1 indicates sellers dominated (Close = Low, Open = High)
/// - 0 indicates balance (Close = Open)
/// - Often smoothed with an SMA (though this implementation provides the raw value)
///
/// Sources:
/// https://www.investopedia.com/terms/b/bop.asp
/// </remarks>
[SkipLocalsInit]
public sealed class Bop : ITValuePublisher
{
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name => "Bop";
public event Action<TValue>? Pub;
/// <summary>
/// Current BOP value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the indicator has a valid value (always true for BOP as it has no warmup).
/// </summary>
public bool IsHot => true;
/// <summary>
/// The number of bars required for the indicator to warm up.
/// </summary>
public int WarmupPeriod => 0;
/// <summary>
/// Resets the indicator state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
Last = default;
}
/// <summary>
/// Updates the indicator with a new bar.
/// </summary>
/// <param name="input">The input bar.</param>
/// <param name="isNew">Whether this is a new bar or an update to the current one.</param>
/// <returns>The updated BOP value.</returns>
[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(Last);
return Last;
}
/// <summary>
/// Updates the indicator with a new value (not supported for BOP as it requires OHLC).
/// </summary>
[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(Last);
return Last;
}
/// <summary>
/// Updates the indicator with a series of bars.
/// </summary>
public TSeries Update(TBarSeries source)
{
return Batch(source);
}
/// <summary>
/// Calculates BOP for a series of bars.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> open, ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, Span<double> 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<double>.Count)
{
var epsilon = new Vector<double>(double.Epsilon);
var vectors = len / Vector<double>.Count;
for (int j = 0; j < vectors; j++)
{
var o = new Vector<double>(open.Slice(i, Vector<double>.Count));
var h = new Vector<double>(high.Slice(i, Vector<double>.Count));
var l = new Vector<double>(low.Slice(i, Vector<double>.Count));
var c = new Vector<double>(close.Slice(i, Vector<double>.Count));
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<double>.Zero);
result.CopyTo(destination.Slice(i, Vector<double>.Count));
i += Vector<double>.Count;
}
}
for (; i < len; i++)
{
double range = high[i] - low[i];
destination[i] = range > double.Epsilon ? (close[i] - open[i]) / range : 0;
}
}
/// <summary>
/// Calculates BOP for a TBarSeries.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TSeries Batch(TBarSeries source)
{
if (source.Count == 0) return new TSeries([], []);
var len = source.Count;
var v = new double[len];
Calculate(source.Open.Values, source.High.Values, source.Low.Values, source.Close.Values, v);
var tList = new List<long>(len);
var times = source.Open.Times;
for (int i = 0; i < len; i++)
{
tList.Add(times[i]);
}
return new TSeries(tList, new List<double>(v));
}
}