// BRAR: Bull-Bear Power Ratio Oscillator // Dual-output sentiment oscillator: AR (Atmosphere Ratio) and BR (Buying Ratio). // Originates from Japanese technical analysis (強弱レシオ). using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// BRAR: Bull-Bear Power Ratio Oscillator /// /// /// Dual-output sentiment oscillator measuring two independent ratios: /// /// BR (Buying Ratio) = SUM(max(0, H − PrevC), N) / SUM(max(0, PrevC − L), N) × 100 /// AR (Atmosphere Ratio) = SUM(max(0, H − O), N) / SUM(max(0, O − L), N) × 100 /// /// Both lines oscillate around 100 (equilibrium). Four O(1) rolling sums via circular /// buffers — 4 additions + 4 subtractions per bar regardless of period length. /// /// First-bar bootstrap: when no previous close exists, the current open is used, /// matching PineScript's nz(close[1], open) behaviour. /// /// References: /// Shimizu, Seiki (1986). The Japanese Chart of Charts. /// PineScript reference: brar.pine /// [SkipLocalsInit] public sealed class Brar : ITValuePublisher { private readonly int _period; // Four circular buffers for O(1) rolling sums private readonly double[] _brNumBuf; private readonly double[] _brDenBuf; private readonly double[] _arNumBuf; private readonly double[] _arDenBuf; // Snapshots of the four buffers saved on each isNew=true call — full array copy // is required because isNew=false must be idempotent across N consecutive calls. // Saving only the overwritten slot is NOT sufficient: _ps is captured before // s.OldXxx is set, so the scalar fields would hold the previous bar's stale value. private readonly double[] _brNumSnap; private readonly double[] _brDenSnap; private readonly double[] _arNumSnap; private readonly double[] _arDenSnap; [StructLayout(LayoutKind.Auto)] private record struct State( double BrNumSum, double BrDenSum, double ArNumSum, double ArDenSum, double PrevClose, double Br, double Ar, int Count, int Idx); private State _s; private State _ps; private readonly TBarPublishedHandler _barHandler; /// Display name for the indicator. public string Name { get; } /// Bars required for the first valid output. public int WarmupPeriod { get; } /// True once the rolling window is fully populated. public bool IsHot => _s.Count >= _period; /// Current AR (Atmosphere Ratio) value. public double Ar => _s.Ar; /// Current BR (Buying Ratio) value. public double Br => _s.Br; /// Primary output (BR as TValue). public TValue Last { get; private set; } public event TValuePublishedHandler? Pub; /// /// Creates BRAR with the specified rolling-window period. /// /// Rolling window length (must be > 0, default 26) public Brar(int period = 26) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } _period = period; _brNumBuf = new double[period]; _brDenBuf = new double[period]; _arNumBuf = new double[period]; _arDenBuf = new double[period]; _brNumSnap = new double[period]; _brDenSnap = new double[period]; _arNumSnap = new double[period]; _arDenSnap = new double[period]; _s = new State(0, 0, 0, 0, double.NaN, 100.0, 100.0, 0, 0); _ps = _s; WarmupPeriod = period; Name = $"Brar({period})"; _barHandler = HandleBar; } /// /// Creates BRAR chained to a TBarSeries source. /// public Brar(TBarSeries source, int period = 26) : this(period) { Prime(source); source.Pub += _barHandler; } private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] private void PubEvent(TValue value, bool isNew) => Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew }); /// Resets all state to initial conditions. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _s = new State(0, 0, 0, 0, double.NaN, 100.0, 100.0, 0, 0); _ps = _s; Last = default; Array.Clear(_brNumBuf); Array.Clear(_brDenBuf); Array.Clear(_arNumBuf); Array.Clear(_arDenBuf); Array.Clear(_brNumSnap); Array.Clear(_brDenSnap); Array.Clear(_arNumSnap); Array.Clear(_arDenSnap); } /// /// Updates BRAR with a new bar. /// /// OHLCV bar data /// True to advance state; false to rewrite the latest bar /// Current BR value as TValue (primary output) [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { var s = _s; if (isNew) { // Snapshot all four buffers before any mutation — required for idempotent // isNew=false rollback across multiple consecutive correction calls. // Saving only the overwritten slot is insufficient: _ps is captured here, // before s.OldXxx would be set, so scalar fields carry the prior bar's stale value. _ps = s; Array.Copy(_brNumBuf, _brNumSnap, _period); Array.Copy(_brDenBuf, _brDenSnap, _period); Array.Copy(_arNumBuf, _arNumSnap, _period); Array.Copy(_arDenBuf, _arDenSnap, _period); s.Count++; } else { // Rollback: restore _ps scalar state + all four buffer snapshots. // Every isNew=false call starts from the identical pre-bar-N state, // so N consecutive correction calls are all idempotent. s = _ps; Array.Copy(_brNumSnap, _brNumBuf, _period); Array.Copy(_brDenSnap, _brDenBuf, _period); Array.Copy(_arNumSnap, _arNumBuf, _period); Array.Copy(_arDenSnap, _arDenBuf, _period); } // Sanitize OHLC inputs — use last-valid on NaN/Infinity double rawOpen = input.Open; double rawHigh = input.High; double rawLow = input.Low; double rawClose = input.Close; double open = double.IsFinite(rawOpen) ? rawOpen : 0.0; double high = double.IsFinite(rawHigh) ? rawHigh : open; double low = double.IsFinite(rawLow) ? rawLow : 0.0; double close = double.IsFinite(rawClose) ? rawClose : open; // First bar: use open as previous close (matches PineScript nz(close[1], open)) double prevClose = double.IsFinite(s.PrevClose) ? s.PrevClose : open; // Compute per-bar contributions (clamped to 0) double brNum = Math.Max(0.0, high - prevClose); double brDen = Math.Max(0.0, prevClose - low); double arNum = Math.Max(0.0, high - open); double arDen = Math.Max(0.0, open - low); // O(1) circular-buffer rolling sums: subtract outgoing, write new, add incoming int idx = s.Idx; s.BrNumSum -= _brNumBuf[idx]; s.BrDenSum -= _brDenBuf[idx]; s.ArNumSum -= _arNumBuf[idx]; s.ArDenSum -= _arDenBuf[idx]; _brNumBuf[idx] = brNum; _brDenBuf[idx] = brDen; _arNumBuf[idx] = arNum; _arDenBuf[idx] = arDen; s.BrNumSum += brNum; s.BrDenSum += brDen; s.ArNumSum += arNum; s.ArDenSum += arDen; // Advance circular index only on new bars if (isNew) { s.Idx = (idx + 1) % _period; } // Compute ratios — default to 100 (equilibrium) on zero denominator s.Br = s.BrDenSum != 0.0 ? s.BrNumSum / s.BrDenSum * 100.0 : 100.0; s.Ar = s.ArDenSum != 0.0 ? s.ArNumSum / s.ArDenSum * 100.0 : 100.0; // Store close for next bar's prevClose s.PrevClose = close; _s = s; Last = new TValue(input.Time, s.Br); PubEvent(Last, isNew); return Last; } /// /// Updates BRAR from a TBarSeries, computing BR and AR series. /// public (TSeries Br, TSeries Ar) UpdateAll(TBarSeries source) { int len = source.Count; if (len == 0) { return (new TSeries([], []), new TSeries([], [])); } var brList = new List(len); var arList = new List(len); CollectionsMarshal.SetCount(brList, len); CollectionsMarshal.SetCount(arList, len); var brSpan = CollectionsMarshal.AsSpan(brList); var arSpan = CollectionsMarshal.AsSpan(arList); Batch( source.Open.Values, source.High.Values, source.Low.Values, source.Close.Values, brSpan, arSpan, _period); var tList = new List(len); CollectionsMarshal.SetCount(tList, len); source.Open.Times.CopyTo(CollectionsMarshal.AsSpan(tList)); // Replay to synchronise internal state Reset(); for (int i = 0; i < len; i++) { Update(source[i], isNew: true); } return (new TSeries(tList, brList), new TSeries(tList, arList)); } /// /// Batch-computes BRAR over raw OHLC spans. Zero-allocation path for large datasets. /// /// Source open prices /// Source high prices /// Source low prices /// Source close prices /// Destination span for BR values /// Destination span for AR values /// Rolling window length (must be > 0) public static void Batch( ReadOnlySpan open, ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, Span brOutput, Span arOutput, int period = 26) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } int len = open.Length; if (high.Length != len) { throw new ArgumentException("High length must match open length", nameof(high)); } if (low.Length != len) { throw new ArgumentException("Low length must match open length", nameof(low)); } if (close.Length != len) { throw new ArgumentException("Close length must match open length", nameof(close)); } if (brOutput.Length != len) { throw new ArgumentException("brOutput length must match input length", nameof(brOutput)); } if (arOutput.Length != len) { throw new ArgumentException("arOutput length must match input length", nameof(arOutput)); } if (len == 0) { return; } const int StackallocThreshold = 256; // Four circular buffers — stack for small periods, ArrayPool for large double[]? rentedBrNum = null; double[]? rentedBrDen = null; double[]? rentedArNum = null; double[]? rentedArDen = null; scoped Span brNumBuf; scoped Span brDenBuf; scoped Span arNumBuf; scoped Span arDenBuf; if (period <= StackallocThreshold) { brNumBuf = stackalloc double[period]; brDenBuf = stackalloc double[period]; arNumBuf = stackalloc double[period]; arDenBuf = stackalloc double[period]; } else { rentedBrNum = ArrayPool.Shared.Rent(period); rentedBrDen = ArrayPool.Shared.Rent(period); rentedArNum = ArrayPool.Shared.Rent(period); rentedArDen = ArrayPool.Shared.Rent(period); brNumBuf = rentedBrNum.AsSpan(0, period); brDenBuf = rentedBrDen.AsSpan(0, period); arNumBuf = rentedArNum.AsSpan(0, period); arDenBuf = rentedArDen.AsSpan(0, period); } try { brNumBuf.Clear(); brDenBuf.Clear(); arNumBuf.Clear(); arDenBuf.Clear(); double brNumSum = 0.0; double brDenSum = 0.0; double arNumSum = 0.0; double arDenSum = 0.0; double prevClose = double.NaN; int idx = 0; for (int i = 0; i < len; i++) { double o = open[i]; double h = high[i]; double l = low[i]; double c = close[i]; // First bar: use open as prevClose if no prior close available double pc = double.IsFinite(prevClose) ? prevClose : o; double brNum = Math.Max(0.0, h - pc); double brDen = Math.Max(0.0, pc - l); double arNum = Math.Max(0.0, h - o); double arDen = Math.Max(0.0, o - l); brNumSum -= brNumBuf[idx]; brDenSum -= brDenBuf[idx]; arNumSum -= arNumBuf[idx]; arDenSum -= arDenBuf[idx]; brNumBuf[idx] = brNum; brDenBuf[idx] = brDen; arNumBuf[idx] = arNum; arDenBuf[idx] = arDen; brNumSum += brNum; brDenSum += brDen; arNumSum += arNum; arDenSum += arDen; idx = (idx + 1) % period; prevClose = c; brOutput[i] = brDenSum != 0.0 ? brNumSum / brDenSum * 100.0 : 100.0; arOutput[i] = arDenSum != 0.0 ? arNumSum / arDenSum * 100.0 : 100.0; } } finally { if (rentedBrNum != null) { ArrayPool.Shared.Return(rentedBrNum); } if (rentedBrDen != null) { ArrayPool.Shared.Return(rentedBrDen); } if (rentedArNum != null) { ArrayPool.Shared.Return(rentedArNum); } if (rentedArDen != null) { ArrayPool.Shared.Return(rentedArDen); } } } /// Primes the indicator by replaying historical data without firing events. public void Prime(TBarSeries source) { foreach (var bar in source) { Update(bar, isNew: true); } } }