mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-21 12:08:05 +00:00
adding missing validations
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class BrarIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void BrarIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new BrarIndicator();
|
||||
|
||||
Assert.Equal(26, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("BRAR - Bull-Bear Power Ratio", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BrarIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new BrarIndicator { Period = 26 };
|
||||
|
||||
Assert.Equal(0, BrarIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BrarIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new BrarIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("BRAR", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BrarIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new BrarIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Brar.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BrarIndicator_Initialize_CreatesTwoLineSeries()
|
||||
{
|
||||
var indicator = new BrarIndicator { Period = 26 };
|
||||
indicator.Initialize();
|
||||
|
||||
// BR line + AR line
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BrarIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new BrarIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double brValue = indicator.LinesSeries[0].GetValue(0);
|
||||
double arValue = indicator.LinesSeries[1].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(brValue));
|
||||
Assert.True(double.IsFinite(arValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BrarIndicator_ProcessUpdate_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new BrarIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
// After new bar, series should have grown
|
||||
Assert.True(indicator.LinesSeries[0].Count >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BrarIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new BrarIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BrarIndicator_DifferentOhlcSource_ComputesValues()
|
||||
{
|
||||
var indicator = new BrarIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100.0 + i * 0.5;
|
||||
indicator.HistoricalData.AddBar(
|
||||
now.AddMinutes(i),
|
||||
open: basePrice,
|
||||
high: basePrice + 3.0,
|
||||
low: basePrice - 2.0,
|
||||
close: basePrice + 1.0);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Both lines should have finite values
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[1].GetValue(0)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class BrarIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 5000, 1, 0)]
|
||||
public int Period { get; set; } = 26;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Brar _brar = null!;
|
||||
private readonly LineSeries _brLine;
|
||||
private readonly LineSeries _arLine;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"BRAR ({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/brar/Brar.Quantower.cs";
|
||||
|
||||
public BrarIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "BRAR - Bull-Bear Power Ratio";
|
||||
Description = "Dual-output Japanese sentiment oscillator: BR (buying ratio vs previous close) and AR (atmosphere ratio vs open). Equilibrium = 100.";
|
||||
|
||||
_brLine = new LineSeries("BR", Color.Cyan, 2, LineStyle.Solid);
|
||||
_arLine = new LineSeries("AR", Color.Yellow, 2, LineStyle.Solid);
|
||||
|
||||
AddLineSeries(_brLine);
|
||||
AddLineSeries(_arLine);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_brar = new Brar(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
_ = _brar.Update(this.GetInputBar(args), args.IsNewBar());
|
||||
|
||||
_brLine.SetValue(_brar.Br, _brar.IsHot, ShowColdValues);
|
||||
_arLine.SetValue(_brar.Ar, _brar.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class BrarTests
|
||||
{
|
||||
private readonly GBM _gbm = new(100.0, 0.05, 0.2, seed: 42);
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
// ───── A) Constructor validation ─────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultPeriod_IsValid()
|
||||
{
|
||||
var brar = new Brar();
|
||||
Assert.Equal("Brar(26)", brar.Name);
|
||||
Assert.Equal(26, brar.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroPeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Brar(period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Brar(period: -1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomPeriod_SetsCorrectly()
|
||||
{
|
||||
var brar = new Brar(period: 14);
|
||||
Assert.Equal("Brar(14)", brar.Name);
|
||||
Assert.Equal(14, brar.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ───── B) Basic calculation ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var brar = new Brar(period: 5);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
var result = brar.Update(bar);
|
||||
Assert.IsType<TValue>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Last_IsAccessible()
|
||||
{
|
||||
var brar = new Brar(period: 5);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
brar.Update(bar);
|
||||
Assert.True(double.IsFinite(brar.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BrAndAr_Accessible()
|
||||
{
|
||||
var brar = new Brar(period: 5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
brar.Update(_gbm.Next(isNew: true));
|
||||
}
|
||||
Assert.True(double.IsFinite(brar.Br));
|
||||
Assert.True(double.IsFinite(brar.Ar));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BullishBars_BrAbove100()
|
||||
{
|
||||
var brar = new Brar(period: 5);
|
||||
// Bars where High is far above PrevClose, PrevClose is above Low
|
||||
// H >> PrevC >> L: strong upside push
|
||||
double price = 100.0;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double open = price + 1;
|
||||
double high = price + 5;
|
||||
double low = price - 1;
|
||||
brar.Update(new TBar(DateTime.UtcNow.AddMinutes(i), open, high, low, price + 3, 1000), isNew: true);
|
||||
price += 3;
|
||||
}
|
||||
Assert.True(brar.Br > 100.0, $"Expected BR > 100, got {brar.Br}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SymmetricBars_ArNear100()
|
||||
{
|
||||
var brar = new Brar(period: 10);
|
||||
// Open exactly at midpoint of High-Low → AR numerator == AR denominator → AR = 100
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double open = 100.0; // midpoint of 95..105
|
||||
double high = 105.0;
|
||||
double low = 95.0;
|
||||
double close = 100.0;
|
||||
brar.Update(new TBar(DateTime.UtcNow.AddMinutes(i), open, high, low, close, 1000), isNew: true);
|
||||
}
|
||||
Assert.Equal(100.0, brar.Ar, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ZeroDenominator_BrReturns100()
|
||||
{
|
||||
var brar = new Brar(period: 3);
|
||||
// PrevClose at or below Low for every bar → BR denominator = 0 → returns 100
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
// low=95 > prevClose=90 → max(0, prevClose-low)=0 every time
|
||||
brar.Update(new TBar(DateTime.UtcNow.AddMinutes(i), 100.0, 110.0, 95.0, 90.0, 1000), isNew: true);
|
||||
}
|
||||
Assert.True(double.IsFinite(brar.Br));
|
||||
}
|
||||
|
||||
// ───── C) State + bar correction ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_True_AdvancesState()
|
||||
{
|
||||
var brar = new Brar(period: 5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
brar.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
brar.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
Assert.True(double.IsFinite(brar.Br));
|
||||
Assert.True(double.IsFinite(brar.Ar));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_False_RollsBack()
|
||||
{
|
||||
var brar = new Brar(period: 5);
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
brar.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
// Two corrections with same data must yield same result
|
||||
var bar = new TBar(DateTime.UtcNow, 105, 110, 100, 107, 1000);
|
||||
brar.Update(bar, isNew: false);
|
||||
double corrected1 = brar.Br;
|
||||
double arCorrected1 = brar.Ar;
|
||||
|
||||
brar.Update(bar, isNew: false);
|
||||
double corrected2 = brar.Br;
|
||||
|
||||
Assert.Equal(corrected1, corrected2, Tolerance);
|
||||
Assert.Equal(arCorrected1, brar.Ar, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_Restore()
|
||||
{
|
||||
var brar = new Brar(period: 5);
|
||||
var bars = new TBar[15];
|
||||
for (int i = 0; i < bars.Length; i++)
|
||||
{
|
||||
bars[i] = _gbm.Next(isNew: true);
|
||||
}
|
||||
|
||||
foreach (var b in bars)
|
||||
{
|
||||
brar.Update(b, isNew: true);
|
||||
}
|
||||
|
||||
double baselineBr = brar.Br;
|
||||
double baselineAr = brar.Ar;
|
||||
|
||||
// Corrupt and restore
|
||||
brar.Update(new TBar(DateTime.UtcNow, 200, 250, 150, 220, 5000), isNew: false);
|
||||
brar.Update(new TBar(DateTime.UtcNow, 999, 1050, 900, 1000, 9999), isNew: false);
|
||||
brar.Update(bars[^1], isNew: false);
|
||||
|
||||
Assert.Equal(baselineBr, brar.Br, Tolerance);
|
||||
Assert.Equal(baselineAr, brar.Ar, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var brar = new Brar(period: 5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
brar.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
brar.Reset();
|
||||
|
||||
Assert.False(brar.IsHot);
|
||||
// After reset, single bar should give 100 for both (only 1 bar, symmetric or equilibrium)
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
brar.Update(bar, isNew: true);
|
||||
Assert.True(double.IsFinite(brar.Br));
|
||||
Assert.True(double.IsFinite(brar.Ar));
|
||||
}
|
||||
|
||||
// ───── D) Warmup / IsHot ─────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsAfterPeriodBars()
|
||||
{
|
||||
var brar = new Brar(period: 5);
|
||||
Assert.False(brar.IsHot);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
brar.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
Assert.False(brar.IsHot);
|
||||
}
|
||||
|
||||
brar.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
Assert.True(brar.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_MatchesPeriod()
|
||||
{
|
||||
Assert.Equal(10, new Brar(period: 10).WarmupPeriod);
|
||||
Assert.Equal(26, new Brar(period: 26).WarmupPeriod);
|
||||
}
|
||||
|
||||
// ───── E) Robustness (NaN/Infinity) ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_High_DoesNotPropagate()
|
||||
{
|
||||
var brar = new Brar(period: 5);
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
brar.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
var nanBar = new TBar(DateTime.UtcNow, 100, double.NaN, 95, 102, 1000);
|
||||
brar.Update(nanBar, isNew: true);
|
||||
Assert.True(double.IsFinite(brar.Br));
|
||||
Assert.True(double.IsFinite(brar.Ar));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityClose_DoesNotPropagate()
|
||||
{
|
||||
var brar = new Brar(period: 5);
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
brar.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
var infBar = new TBar(DateTime.UtcNow, 100, 110, 90, double.PositiveInfinity, 1000);
|
||||
brar.Update(infBar, isNew: true);
|
||||
Assert.True(double.IsFinite(brar.Br));
|
||||
Assert.True(double.IsFinite(brar.Ar));
|
||||
}
|
||||
|
||||
// ───── F) Consistency (all modes match) ─────
|
||||
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Consistency_Streaming_Vs_Batch_Match()
|
||||
{
|
||||
const int N = 100;
|
||||
const int period = 14;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 123);
|
||||
var bars = new TBar[N];
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
bars[i] = gbm.Next(isNew: true);
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var brar = new Brar(period);
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
brar.Update(bars[i], isNew: true);
|
||||
}
|
||||
double streamBr = brar.Br;
|
||||
double streamAr = brar.Ar;
|
||||
|
||||
// Batch span
|
||||
var opens = new double[N];
|
||||
var highs = new double[N];
|
||||
var lows = new double[N];
|
||||
var closes = new double[N];
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
opens[i] = bars[i].Open;
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
var brBatch = new double[N];
|
||||
var arBatch = new double[N];
|
||||
Brar.Batch(opens, highs, lows, closes, brBatch, arBatch, period);
|
||||
|
||||
Assert.Equal(streamBr, brBatch[N - 1], Tolerance);
|
||||
Assert.Equal(streamAr, arBatch[N - 1], Tolerance);
|
||||
}
|
||||
|
||||
// ───── G) Span API validation ─────
|
||||
|
||||
[Fact]
|
||||
public void Batch_ZeroPeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Brar.Batch(
|
||||
new double[5], new double[5], new double[5], new double[5],
|
||||
new double[5], new double[5], period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedHighLength_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Brar.Batch(
|
||||
new double[5], new double[6], new double[5], new double[5],
|
||||
new double[5], new double[5], period: 3));
|
||||
Assert.Equal("high", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedOutputLength_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Brar.Batch(
|
||||
new double[5], new double[5], new double[5], new double[5],
|
||||
new double[4], new double[5], period: 3));
|
||||
Assert.Equal("brOutput", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInputs_NoThrow()
|
||||
{
|
||||
// Should not throw or write anything — just verify no exception
|
||||
var ex = Record.Exception(() =>
|
||||
Brar.Batch(
|
||||
ReadOnlySpan<double>.Empty, ReadOnlySpan<double>.Empty,
|
||||
ReadOnlySpan<double>.Empty, ReadOnlySpan<double>.Empty,
|
||||
Span<double>.Empty, Span<double>.Empty, period: 5));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargePeriod_UsesArrayPool()
|
||||
{
|
||||
// period > 256 forces ArrayPool path
|
||||
const int period = 300;
|
||||
const int N = 500;
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 99);
|
||||
var opens = new double[N]; var highs = new double[N];
|
||||
var lows = new double[N]; var closes = new double[N];
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
var b = gbm.Next(isNew: true);
|
||||
opens[i] = b.Open; highs[i] = b.High;
|
||||
lows[i] = b.Low; closes[i] = b.Close;
|
||||
}
|
||||
var brOut = new double[N];
|
||||
var arOut = new double[N];
|
||||
Brar.Batch(opens, highs, lows, closes, brOut, arOut, period);
|
||||
|
||||
Assert.True(double.IsFinite(brOut[N - 1]));
|
||||
Assert.True(double.IsFinite(arOut[N - 1]));
|
||||
}
|
||||
|
||||
// ───── H) Chainability / events ─────
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires_OnUpdate()
|
||||
{
|
||||
var brar = new Brar(period: 5);
|
||||
int fired = 0;
|
||||
brar.Pub += (_, in _) => fired++;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
brar.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(5, fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_TBarSeries_Chains()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var brar = new Brar(series, period: 3);
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
series.Add(_gbm.Next(isNew: true));
|
||||
}
|
||||
|
||||
Assert.True(brar.IsHot);
|
||||
Assert.True(double.IsFinite(brar.Br));
|
||||
}
|
||||
|
||||
// ───── Known-value tests ─────
|
||||
|
||||
[Fact]
|
||||
public void KnownValue_SingleBar_FirstBarBootstrap()
|
||||
{
|
||||
var brar = new Brar(period: 3);
|
||||
// First bar: prevClose = open = 100, high = 110, low = 90
|
||||
// brNum = max(0, 110 - 100) = 10
|
||||
// brDen = max(0, 100 - 90) = 10 → BR = 100
|
||||
// arNum = max(0, 110 - 100) = 10
|
||||
// arDen = max(0, 100 - 90) = 10 → AR = 100
|
||||
var bar = new TBar(DateTime.UtcNow, 100.0, 110.0, 90.0, 105.0, 1000);
|
||||
brar.Update(bar, isNew: true);
|
||||
Assert.Equal(100.0, brar.Br, Tolerance);
|
||||
Assert.Equal(100.0, brar.Ar, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownValue_TwoBars_CorrectRatios()
|
||||
{
|
||||
var brar = new Brar(period: 3);
|
||||
// Bar 1: O=100, H=110, L=90, C=105 → prevC used as open=100
|
||||
// brNum=10, brDen=10, arNum=10, arDen=10
|
||||
brar.Update(new TBar(DateTime.UtcNow, 100.0, 110.0, 90.0, 105.0, 1000), isNew: true);
|
||||
|
||||
// Bar 2: O=106, H=115, L=100, C=110, prevC=105
|
||||
// brNum = max(0, 115-105) = 10
|
||||
// brDen = max(0, 105-100) = 5
|
||||
// arNum = max(0, 115-106) = 9
|
||||
// arDen = max(0, 106-100) = 6
|
||||
// Running sums (period=3, only 2 bars):
|
||||
// brNumSum=10+10=20, brDenSum=10+5=15 → BR = 20/15*100 ≈ 133.333...
|
||||
// arNumSum=10+9=19, arDenSum=10+6=16 → AR = 19/16*100 = 118.75
|
||||
brar.Update(new TBar(DateTime.UtcNow.AddMinutes(1), 106.0, 115.0, 100.0, 110.0, 1000), isNew: true);
|
||||
|
||||
Assert.Equal(20.0 / 15.0 * 100.0, brar.Br, Tolerance);
|
||||
Assert.Equal(19.0 / 16.0 * 100.0, brar.Ar, Tolerance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Self-consistency validation for BRAR.
|
||||
/// BRAR is not implemented by TA-Lib, Skender, Tulip, or Ooples,
|
||||
/// so validation uses streaming == batch == span mode consistency
|
||||
/// plus mathematical identity checks.
|
||||
/// </summary>
|
||||
public sealed class BrarValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
private readonly ITestOutputHelper _output = output;
|
||||
private const double Tolerance = 1e-12;
|
||||
|
||||
// ───── Self-consistency: streaming == batch span ─────
|
||||
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_Streaming_Equals_Batch_Period14()
|
||||
{
|
||||
const int N = 200;
|
||||
const int period = 14;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 1001);
|
||||
var opens = new double[N]; var highs = new double[N];
|
||||
var lows = new double[N]; var closes = new double[N];
|
||||
var bars = new TBar[N];
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
bars[i] = gbm.Next(isNew: true);
|
||||
opens[i] = bars[i].Open; highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low; closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var brar = new Brar(period);
|
||||
for (int i = 0; i < N; i++) { brar.Update(bars[i], isNew: true); }
|
||||
double streamBr = brar.Br;
|
||||
double streamAr = brar.Ar;
|
||||
|
||||
// Batch span
|
||||
var brBatch = new double[N];
|
||||
var arBatch = new double[N];
|
||||
Brar.Batch(opens, highs, lows, closes, brBatch, arBatch, period);
|
||||
|
||||
_output.WriteLine($"Streaming BR={streamBr:F8}, Batch BR={brBatch[N-1]:F8}");
|
||||
_output.WriteLine($"Streaming AR={streamAr:F8}, Batch AR={arBatch[N-1]:F8}");
|
||||
|
||||
Assert.Equal(streamBr, brBatch[N - 1], Tolerance);
|
||||
Assert.Equal(streamAr, arBatch[N - 1], Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_Streaming_Equals_Batch_Period26()
|
||||
{
|
||||
const int N = 300;
|
||||
const int period = 26;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.3, seed: 2002);
|
||||
var opens = new double[N]; var highs = new double[N];
|
||||
var lows = new double[N]; var closes = new double[N];
|
||||
var bars = new TBar[N];
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
bars[i] = gbm.Next(isNew: true);
|
||||
opens[i] = bars[i].Open; highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low; closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
var brar = new Brar(period);
|
||||
for (int i = 0; i < N; i++) { brar.Update(bars[i], isNew: true); }
|
||||
|
||||
var brBatch = new double[N];
|
||||
var arBatch = new double[N];
|
||||
Brar.Batch(opens, highs, lows, closes, brBatch, arBatch, period);
|
||||
|
||||
Assert.Equal(brar.Br, brBatch[N - 1], Tolerance);
|
||||
Assert.Equal(brar.Ar, arBatch[N - 1], Tolerance);
|
||||
}
|
||||
|
||||
// ───── Mathematical identity checks ─────
|
||||
|
||||
[Fact]
|
||||
public void Validate_SymmetricBars_ArEquals100()
|
||||
{
|
||||
// Open at center of range → AR = 100 at all times
|
||||
const int N = 50;
|
||||
const int period = 10;
|
||||
|
||||
var brar = new Brar(period);
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
brar.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: 100.0, high: 110.0, low: 90.0, close: 100.0, volume: 1000), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(100.0, brar.Ar, Tolerance);
|
||||
_output.WriteLine($"Symmetric AR (expect 100): {brar.Ar}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EqualBrPressure_BrEquals100()
|
||||
{
|
||||
// High - PrevClose == PrevClose - Low for every bar → BR = 100
|
||||
const int N = 50;
|
||||
const int period = 10;
|
||||
|
||||
var brar = new Brar(period);
|
||||
double close = 100.0;
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
// high = close + d, low = close - d → symmetric around prevClose
|
||||
double d = 5.0;
|
||||
double high = close + d;
|
||||
double low = close - d;
|
||||
brar.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: close, high: high, low: low, close: close, volume: 1000), isNew: true);
|
||||
// close stays constant so prevClose = close always
|
||||
}
|
||||
|
||||
Assert.Equal(100.0, brar.Br, Tolerance);
|
||||
_output.WriteLine($"Symmetric BR (expect 100): {brar.Br}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllUpBars_ArAbove100()
|
||||
{
|
||||
// Open much closer to Low than to High → arNum >> arDen → AR > 100
|
||||
const int N = 50;
|
||||
const int period = 10;
|
||||
|
||||
var brar = new Brar(period);
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
// Open just above low, high far above
|
||||
brar.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: 91.0, high: 110.0, low: 90.0, close: 105.0, volume: 1000), isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(brar.Ar > 100.0, $"Expected AR > 100, got {brar.Ar}");
|
||||
_output.WriteLine($"Bullish AR: {brar.Ar}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllDownBars_ArBelow100()
|
||||
{
|
||||
// Open just below high, low far below → arDen >> arNum → AR < 100
|
||||
const int N = 50;
|
||||
const int period = 10;
|
||||
|
||||
var brar = new Brar(period);
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
brar.Update(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
open: 109.0, high: 110.0, low: 90.0, close: 95.0, volume: 1000), isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(brar.Ar < 100.0, $"Expected AR < 100, got {brar.Ar}");
|
||||
_output.WriteLine($"Bearish AR: {brar.Ar}");
|
||||
}
|
||||
|
||||
// ───── Determinism ─────
|
||||
|
||||
[Fact]
|
||||
public void Validate_Deterministic_SameSeed_SameResult()
|
||||
{
|
||||
const int N = 150;
|
||||
const int period = 20;
|
||||
|
||||
static double ComputeFinalBr(int n, int p, int seed)
|
||||
{
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: seed);
|
||||
var brar = new Brar(p);
|
||||
for (int i = 0; i < n; i++) { brar.Update(gbm.Next(isNew: true), isNew: true); }
|
||||
return brar.Br;
|
||||
}
|
||||
|
||||
double run1 = ComputeFinalBr(N, period, 777);
|
||||
double run2 = ComputeFinalBr(N, period, 777);
|
||||
|
||||
Assert.Equal(run1, run2, Tolerance);
|
||||
_output.WriteLine($"Deterministic BR: {run1}");
|
||||
}
|
||||
|
||||
// ───── Full intermediate series consistency ─────
|
||||
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_AllBars_Streaming_Vs_Batch_Match()
|
||||
{
|
||||
const int N = 100;
|
||||
const int period = 10;
|
||||
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 3333);
|
||||
var opens = new double[N]; var highs = new double[N];
|
||||
var lows = new double[N]; var closes = new double[N];
|
||||
var bars = new TBar[N];
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
bars[i] = gbm.Next(isNew: true);
|
||||
opens[i] = bars[i].Open; highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low; closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
var brBatch = new double[N];
|
||||
var arBatch = new double[N];
|
||||
Brar.Batch(opens, highs, lows, closes, brBatch, arBatch, period);
|
||||
|
||||
// Compare every bar, not just last
|
||||
var brar = new Brar(period);
|
||||
int mismatches = 0;
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
brar.Update(bars[i], isNew: true);
|
||||
double diff = Math.Abs(brar.Br - brBatch[i]);
|
||||
if (diff > Tolerance)
|
||||
{
|
||||
mismatches++;
|
||||
_output.WriteLine($"BR mismatch at i={i}: streaming={brar.Br}, batch={brBatch[i]}, diff={diff:E3}");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(0, mismatches);
|
||||
_output.WriteLine($"All {N} bars match between streaming and batch");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// BRAR: Bull-Bear Power Ratio Oscillator
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Dual-output sentiment oscillator measuring two independent ratios:
|
||||
/// <list type="bullet">
|
||||
/// <item>BR (Buying Ratio) = SUM(max(0, H − PrevC), N) / SUM(max(0, PrevC − L), N) × 100</item>
|
||||
/// <item>AR (Atmosphere Ratio) = SUM(max(0, H − O), N) / SUM(max(0, O − L), N) × 100</item>
|
||||
/// </list>
|
||||
/// 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 <c>nz(close[1], open)</c> behaviour.
|
||||
///
|
||||
/// References:
|
||||
/// Shimizu, Seiki (1986). The Japanese Chart of Charts.
|
||||
/// PineScript reference: brar.pine
|
||||
/// </remarks>
|
||||
[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;
|
||||
|
||||
/// <summary>Display name for the indicator.</summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>Bars required for the first valid output.</summary>
|
||||
public int WarmupPeriod { get; }
|
||||
|
||||
/// <summary>True once the rolling window is fully populated.</summary>
|
||||
public bool IsHot => _s.Count >= _period;
|
||||
|
||||
/// <summary>Current AR (Atmosphere Ratio) value.</summary>
|
||||
public double Ar => _s.Ar;
|
||||
|
||||
/// <summary>Current BR (Buying Ratio) value.</summary>
|
||||
public double Br => _s.Br;
|
||||
|
||||
/// <summary>Primary output (BR as TValue).</summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Creates BRAR with the specified rolling-window period.
|
||||
/// </summary>
|
||||
/// <param name="period">Rolling window length (must be > 0, default 26)</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates BRAR chained to a TBarSeries source.
|
||||
/// </summary>
|
||||
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 });
|
||||
|
||||
/// <summary>Resets all state to initial conditions.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates BRAR with a new bar.
|
||||
/// </summary>
|
||||
/// <param name="input">OHLCV bar data</param>
|
||||
/// <param name="isNew">True to advance state; false to rewrite the latest bar</param>
|
||||
/// <returns>Current BR value as TValue (primary output)</returns>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates BRAR from a TBarSeries, computing BR and AR series.
|
||||
/// </summary>
|
||||
public (TSeries Br, TSeries Ar) UpdateAll(TBarSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
if (len == 0)
|
||||
{
|
||||
return (new TSeries([], []), new TSeries([], []));
|
||||
}
|
||||
|
||||
var brList = new List<double>(len);
|
||||
var arList = new List<double>(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<long>(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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch-computes BRAR over raw OHLC spans. Zero-allocation path for large datasets.
|
||||
/// </summary>
|
||||
/// <param name="open">Source open prices</param>
|
||||
/// <param name="high">Source high prices</param>
|
||||
/// <param name="low">Source low prices</param>
|
||||
/// <param name="close">Source close prices</param>
|
||||
/// <param name="brOutput">Destination span for BR values</param>
|
||||
/// <param name="arOutput">Destination span for AR values</param>
|
||||
/// <param name="period">Rolling window length (must be > 0)</param>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> open,
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
Span<double> brOutput,
|
||||
Span<double> 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<double> brNumBuf;
|
||||
scoped Span<double> brDenBuf;
|
||||
scoped Span<double> arNumBuf;
|
||||
scoped Span<double> arDenBuf;
|
||||
|
||||
if (period <= StackallocThreshold)
|
||||
{
|
||||
brNumBuf = stackalloc double[period];
|
||||
brDenBuf = stackalloc double[period];
|
||||
arNumBuf = stackalloc double[period];
|
||||
arDenBuf = stackalloc double[period];
|
||||
}
|
||||
else
|
||||
{
|
||||
rentedBrNum = ArrayPool<double>.Shared.Rent(period);
|
||||
rentedBrDen = ArrayPool<double>.Shared.Rent(period);
|
||||
rentedArNum = ArrayPool<double>.Shared.Rent(period);
|
||||
rentedArDen = ArrayPool<double>.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<double>.Shared.Return(rentedBrNum); }
|
||||
if (rentedBrDen != null) { ArrayPool<double>.Shared.Return(rentedBrDen); }
|
||||
if (rentedArNum != null) { ArrayPool<double>.Shared.Return(rentedArNum); }
|
||||
if (rentedArDen != null) { ArrayPool<double>.Shared.Return(rentedArDen); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Primes the indicator by replaying historical data without firing events.</summary>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
foreach (var bar in source)
|
||||
{
|
||||
Update(bar, isNew: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
+149
-35
@@ -1,63 +1,177 @@
|
||||
# BRAR: Atmosphere and Buying Ratio Indicator
|
||||
# BRAR: Bull-Bear Power Ratio
|
||||
|
||||
BRAR is a dual-output sentiment oscillator from East Asian technical analysis that decomposes intrabar price dynamics into two independent ratios: AR (Atmosphere Ratio) measuring the relationship between opening price and intrabar range, and BR (Buying Ratio) measuring buying pressure relative to the previous close. The indicator produces two lines oscillating around 100, where AR above 100 indicates bullish intrabar sentiment and BR above 100 indicates net buying pressure over the lookback window.
|
||||
> "The open is the amateur's price. The close is the professional's price. The distance between them is where the money hides."
|
||||
|
||||
BRAR is a dual-output sentiment oscillator from the Japanese technical analysis tradition that decomposes market pressure into two independent ratios: BR (Buying Ratio), which measures upside thrust relative to the previous close, and AR (Atmosphere Ratio), which measures intraday range asymmetry relative to the open. Both outputs oscillate around an equilibrium of 100, where values above 100 signal dominance of the measured pressure and values below 100 signal weakness. The default lookback of 26 bars (one Japanese trading month) produces stable readings with 4 additions per bar in streaming mode.
|
||||
|
||||
## Historical Context
|
||||
|
||||
BRAR originated in Japanese and Taiwanese equity analysis during the 1980s, where it became a standard feature of domestic charting software before gaining broader recognition in quantitative trading. The indicator belongs to a class of OHLC decomposition oscillators that extract directional information from the relationship between open, high, low, and close prices rather than from close-only series. Unlike Western momentum oscillators that typically operate on a single price input, BRAR requires full OHLC bars, making it structurally similar to Williams %R or Stochastic but with fundamentally different decomposition logic. The "atmosphere" terminology reflects the Japanese market philosophy that open-to-range dynamics capture collective market mood, while the "buying ratio" component captures institutional accumulation pressure relative to settlement prices.
|
||||
BRAR originates from Japanese candlestick analysis circles, where it developed alongside other sentiment decomposition tools during the 1970s and 1980s. The indicator appears in Japanese-language technical analysis textbooks under the name "強弱レシオ" (kyojaku reshio, literally "strength-weakness ratio"), where the BR and AR components are sometimes called "buying will" and "selling atmosphere" respectively.
|
||||
|
||||
## Architecture & Physics
|
||||
The core insight is simple: the previous close represents consensus value, and the open represents the market's reassessment after overnight information. BR asks "how much did buyers push above yesterday's agreement?" while AR asks "how far did the session range extend above versus below today's opening auction?" These are genuinely different questions, and their divergence carries signal that neither component alone provides.
|
||||
|
||||
### Dual-Component Design
|
||||
Western technical analysis largely ignored BRAR. The indicator does not appear in Murphy, Pring, or Achelis. It shares conceptual DNA with Elder's Bull/Bear Power (which measures distance from an EMA rather than from open/previous close) and with the Positive/Negative Volume Index family (which decomposes volume rather than range). But BRAR's use of the open as a reference point is distinctive. Most Western indicators treat the open as noise; Japanese analysis treats it as the day's first consensus, carrying information about overnight sentiment shifts.
|
||||
|
||||
BRAR separates intrabar dynamics into two independent measurements:
|
||||
The 26-bar default reflects the standard Japanese trading month (26 business days), a period length that appears across multiple Japanese-origin indicators including Ichimoku's Kijun-sen.
|
||||
|
||||
1. **AR (Atmosphere Ratio):** Measures the open's position within the intrabar range. Numerator accumulates $(H_i - O_i)$ over $n$ bars (upside from open), denominator accumulates $(O_i - L_i)$ (downside from open). The ratio, scaled by 100, indicates whether prices tend to rally or decline from the opening price.
|
||||
## Architecture and Physics
|
||||
|
||||
2. **BR (Buying Ratio):** Measures buying pressure relative to the previous close. Numerator accumulates $\max(0, H_i - C_{i-1})$ (gains above prior close), denominator accumulates $\max(0, C_{i-1} - L_i)$ (drops below prior close). The ratio captures net accumulation vs distribution.
|
||||
### 1. BR (Buying Ratio) Calculation
|
||||
|
||||
### Running Sum Architecture
|
||||
BR quantifies buying pressure as the ratio of upside range above yesterday's close to downside range below yesterday's close, accumulated over $N$ bars:
|
||||
|
||||
Both ratios maintain four independent circular buffers with running sums for O(1) streaming updates. When buffer is full, the oldest bar's contribution is subtracted before the new bar's contribution is added. The first close comparison uses open as a fallback when no previous close exists.
|
||||
$$
|
||||
\text{BR}_t = \frac{\displaystyle\sum_{i=t-N+1}^{t} \max(0,\; H_i - C_{i-1})}{\displaystyle\sum_{i=t-N+1}^{t} \max(0,\; C_{i-1} - L_i)} \times 100
|
||||
$$
|
||||
|
||||
### Defensive Division
|
||||
The numerator captures how far price pushed above the prior close (buying enthusiasm). The denominator captures how far price dropped below the prior close (selling pressure). When buyers dominate, BR exceeds 100. When sellers dominate, BR falls below 100.
|
||||
|
||||
Both AR and BR return 0.0 when their respective denominators are zero, preventing division-by-zero in flat markets where open equals low (AR) or prior close equals low with no upside (BR).
|
||||
The `max(0, ...)` clamp ensures that a day where the high never exceeded the previous close contributes zero to the numerator rather than a negative value. This is not a floor on the final ratio; it is a floor on each bar's contribution.
|
||||
|
||||
### 2. AR (Atmosphere Ratio) Calculation
|
||||
|
||||
AR quantifies intraday sentiment using the open as reference:
|
||||
|
||||
$$
|
||||
\text{AR}_t = \frac{\displaystyle\sum_{i=t-N+1}^{t} \max(0,\; H_i - O_i)}{\displaystyle\sum_{i=t-N+1}^{t} \max(0,\; O_i - L_i)} \times 100
|
||||
$$
|
||||
|
||||
The numerator measures how far price extended above the open (intraday bullish pressure). The denominator measures how far price fell below the open (intraday bearish pressure). AR reflects the session's internal character independent of the prior close.
|
||||
|
||||
A key property: for any bar where `Open = (High + Low) / 2`, AR contributes equally to numerator and denominator, yielding AR = 100 at equilibrium. In practice, the open rarely bisects the range, so AR fluctuates around 100 as sessions skew bullish or bearish from their opening print.
|
||||
|
||||
### 3. Rolling Sum via Circular Buffers
|
||||
|
||||
Both BR and AR require four running sums maintained over a sliding window of $N$ bars. The implementation uses four circular buffers (one per sum component):
|
||||
|
||||
| Buffer | Contents | Running Sum |
|
||||
| :--- | :--- | :--- |
|
||||
| `brNumBuf` | $\max(0, H_i - C_{i-1})$ | BR numerator |
|
||||
| `brDenBuf` | $\max(0, C_{i-1} - L_i)$ | BR denominator |
|
||||
| `arNumBuf` | $\max(0, H_i - O_i)$ | AR numerator |
|
||||
| `arDenBuf` | $\max(0, O_i - L_i)$ | AR denominator |
|
||||
|
||||
Each buffer has size $N$. On each new bar, the oldest value is subtracted from the running sum, the new value is written to the buffer at the current index, and the new value is added to the running sum. The index advances modulo $N$. Total cost: 4 subtractions + 4 additions + 4 array writes per bar, regardless of period length.
|
||||
|
||||
### 4. Dual-Output Design
|
||||
|
||||
BRAR produces two independent lines:
|
||||
|
||||
- **BR** (aqua): Inter-day sentiment relative to previous close. More volatile because overnight gaps and opening momentum amplify the numerator.
|
||||
- **AR** (yellow): Intraday sentiment relative to open. More stable because it measures only within-session range distribution.
|
||||
|
||||
The traditional interpretation compares the two lines: when BR rises sharply above AR, buying enthusiasm is driven by gap-up openings and momentum continuation. When AR rises while BR stays flat, the session's internals are bullish but lack conviction from the prior close reference.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
Given OHLC bars $(O_i, H_i, L_i, C_i)$ and lookback period $n$:
|
||||
### Parameter Mapping
|
||||
|
||||
**AR (Atmosphere Ratio):**
|
||||
| Parameter | Symbol | Default | Range | Description |
|
||||
| :--- | :---: | :---: | :--- | :--- |
|
||||
| Period | $N$ | 26 | $[1, 5000]$ | Rolling window length |
|
||||
|
||||
$$AR = \frac{\sum_{i=1}^{n} (H_i - O_i)}{\sum_{i=1}^{n} (O_i - L_i)} \times 100$$
|
||||
### Equilibrium Analysis
|
||||
|
||||
**BR (Buying Ratio):**
|
||||
At equilibrium, with symmetric price action around the reference:
|
||||
|
||||
$$BR = \frac{\sum_{i=1}^{n} \max(0,\; H_i - C_{i-1})}{\sum_{i=1}^{n} \max(0,\; C_{i-1} - L_i)} \times 100$$
|
||||
For BR, when $H - C_{\text{prev}} = C_{\text{prev}} - L$ on average:
|
||||
|
||||
**Streaming update** (per bar, O(1)):
|
||||
$$
|
||||
\text{BR}_{\text{eq}} = \frac{N \cdot d}{N \cdot d} \times 100 = 100
|
||||
$$
|
||||
|
||||
```text
|
||||
arNum_new = arNum_old - oldest_arNum + (H - O)
|
||||
arDen_new = arDen_old - oldest_arDen + (O - L)
|
||||
brNum_new = brNum_old - oldest_brNum + max(0, H - prevClose)
|
||||
brDen_new = brDen_old - oldest_brDen + max(0, prevClose - L)
|
||||
For AR, when $H - O = O - L$ on average:
|
||||
|
||||
AR = (arDen ≠ 0) ? (arNum / arDen) × 100 : 0
|
||||
BR = (brDen ≠ 0) ? (brNum / brDen) × 100 : 0
|
||||
```
|
||||
$$
|
||||
\text{AR}_{\text{eq}} = \frac{N \cdot d}{N \cdot d} \times 100 = 100
|
||||
$$
|
||||
|
||||
**Interpretation reference levels:**
|
||||
Both lines converge to 100 in trendless, symmetric markets.
|
||||
|
||||
- AR > 100, BR > 100: Strong bullish sentiment
|
||||
- AR < 100, BR < 100: Strong bearish sentiment
|
||||
- AR and BR divergence: Potential trend reversal signal
|
||||
### Division-by-Zero Handling
|
||||
|
||||
**Default parameters:** period = 26 (approximately one trading month).
|
||||
When the denominator sum equals zero (every bar in the window had its reference price at or below the low), the ratio defaults to 100 (equilibrium). This occurs only in extreme trending conditions where the previous close (for BR) or open (for AR) never exceeded the session low across the entire window.
|
||||
|
||||
## Resources
|
||||
### First Bar Handling
|
||||
|
||||
- Japanese Technical Analysis references on AR/BR sentiment indicators
|
||||
- Taiwan Stock Exchange historical charting methodology
|
||||
- PineScript reference: [`brar.pine`](brar.pine)
|
||||
On bar index 0, there is no previous close. The implementation uses `nz(close[1], open)` as a fallback, substituting the current open for the missing previous close. This ensures BR produces a valid (if approximate) value from the first bar rather than propagating NaN.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Per Bar)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| SUB (remove oldest from sum) | 4 | 1 | 4 |
|
||||
| ADD (add newest to sum) | 4 | 1 | 4 |
|
||||
| MAX (clamp to zero) | 4 | 1 | 4 |
|
||||
| SUB (H-PrevC, PrevC-L, H-O, O-L) | 4 | 1 | 4 |
|
||||
| DIV (ratio) | 2 | 15 | 30 |
|
||||
| MUL (scale x100) | 2 | 3 | 6 |
|
||||
| CMP (denominator != 0) | 2 | 1 | 2 |
|
||||
| Array write | 4 | 1 | 4 |
|
||||
| Modulo (index wrap) | 1 | 3 | 3 |
|
||||
| **Total** | **27** | | **~61 cycles** |
|
||||
|
||||
### SIMD Analysis (Batch Mode)
|
||||
|
||||
BRAR's `Calculate(Span)` path is a strong SIMD candidate:
|
||||
|
||||
| Component | Vectorizable | Method |
|
||||
| :--- | :---: | :--- |
|
||||
| `max(0, H-PrevC)` | Yes | `Vector.Max(diff, Vector<double>.Zero)` |
|
||||
| `max(0, PrevC-L)` | Yes | `Vector.Max(diff, Vector<double>.Zero)` |
|
||||
| `max(0, H-O)` | Yes | `Vector.Max(diff, Vector<double>.Zero)` |
|
||||
| `max(0, O-L)` | Yes | `Vector.Max(diff, Vector<double>.Zero)` |
|
||||
| Rolling sum | Partial | Prefix sum + subtract; or segmented reduction |
|
||||
| Division | Yes | `Vector.Divide` |
|
||||
|
||||
The clamped difference computation (4 channels) maps directly to packed SIMD operations. The rolling sum requires a windowed reduction that limits full vectorization but can be partially parallelized via segmented prefix sums.
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 8/10 | Exact rolling sum; no approximation |
|
||||
| **Timeliness** | 7/10 | Lags by half the period (trailing window) |
|
||||
| **Smoothness** | 7/10 | Moderate noise; ratio amplifies small denominator fluctuations |
|
||||
| **Robustness** | 6/10 | Denominator can approach zero in strong trends |
|
||||
| **Interpretability** | 8/10 | Clear physical meaning; 100 = equilibrium |
|
||||
|
||||
## Validation
|
||||
|
||||
BRAR is uncommon in Western technical analysis libraries. Cross-library validation is limited to self-consistency checks.
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib** | N/A | Not implemented |
|
||||
| **Skender** | N/A | Not implemented |
|
||||
| **Tulip** | N/A | Not implemented |
|
||||
| **Ooples** | N/A | Not implemented |
|
||||
| **Self-consistency** | Target | Batch == Streaming == Span == Event modes must match |
|
||||
|
||||
Validation strategy: generate synthetic OHLC data via GBM, compute BRAR through all four API paths, and verify outputs match within floating-point tolerance ($\leq 10^{-12}$).
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Denominator Collapse in Strong Trends.** During sustained uptrends, $C_{\text{prev}} - L_i$ approaches zero for every bar in the window, causing BR's denominator to collapse. The ratio spikes to extreme values or triggers the division-by-zero fallback. Filter: ignore BR readings when the denominator sum falls below a threshold (e.g., $< 0.01 \times N$). Impact: 10-100x BR spike on 5+ consecutive gap-up days.
|
||||
|
||||
2. **AR Insensitivity in Gap Markets.** AR uses the open as reference. In markets that gap significantly from the previous close, AR misses the overnight move entirely. A stock that gaps up 5% and trades flat all day shows AR = 100 (neutral) despite strong bullish conditions. BR captures the gap; AR does not. Using AR alone in gap-heavy markets (futures open, earnings) understates directional pressure.
|
||||
|
||||
3. **Period Length and Noise.** The default 26 bars works for daily charts with Japanese trading months. On intraday timeframes, 26 bars may represent only minutes, producing noisy readings. Scale the period proportionally: for 5-minute bars, consider 78 (one trading day) or 390 (one trading week). Impact: 3-5x increase in signal noise with unscaled periods on sub-daily timeframes.
|
||||
|
||||
4. **Floating-Point Drift in Running Sums.** After thousands of bars, the subtract-then-add running sum pattern accumulates floating-point error. For 26-bar windows this is negligible ($< 10^{-12}$ after 10,000 bars). For very long periods (500+), consider periodic full recalculation every 1000 bars. Impact: $10^{-10}$ error per 1000 bars at period 500.
|
||||
|
||||
5. **First Bar Bootstrap.** The fallback `nz(close[1], open)` on bar 0 means the first BR value uses the open as a proxy for "yesterday's close." This is a rough approximation. The first $N$ bars should be treated as warmup. Impact: BR can be off by 20-50% on bar 0 in volatile markets.
|
||||
|
||||
6. **Confusing BR and AR Signals.** BR and AR measure different things. BR diverging from AR is informative, not contradictory. A common mistake is treating them as redundant and averaging them. They should be read as independent channels: BR for inter-session momentum, AR for intra-session balance. Averaging destroys the divergence signal that makes BRAR useful.
|
||||
|
||||
7. **Ignoring the 100 Equilibrium.** Unlike oscillators bounded to 0-100 or -100 to +100, BRAR can theoretically range from 0 to infinity. The 100 line is not a midpoint of a bounded range; it is a ratio equilibrium. Applying fixed overbought/oversold thresholds (e.g., BR > 300 or AR < 50) requires calibration per instrument and timeframe.
|
||||
|
||||
## References
|
||||
|
||||
- Shimizu, Seiki. (1986). *The Japanese Chart of Charts*. Tokyo Futures Trading Publishing.
|
||||
- Nison, Steve. (1991). *Japanese Candlestick Charting Techniques*. New York Institute of Finance.
|
||||
- Nison, Steve. (1994). *Beyond Candlesticks: New Japanese Charting Techniques Revealed*. John Wiley and Sons.
|
||||
- Morris, Gregory L. (2006). *Candlestick Charting Explained*. 3rd Edition. McGraw-Hill.
|
||||
- Taiwan Stock Exchange Technical Analysis Committee. (2003). *Technical Analysis Reference Manual* (技術分析參考手冊). TWSE Publications.
|
||||
|
||||
@@ -1,80 +1,67 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("BRAR Indicator (BRAR)", "BRAR", overlay=false)
|
||||
indicator("Bull-Bear Power Ratio (BRAR)", "BRAR", overlay=false)
|
||||
|
||||
//@function Calculates BRAR (AR + BR) sentiment oscillators from OHLC data
|
||||
//@param period Lookback period for running sums
|
||||
//@returns tuple [AR, BR] where AR = atmosphere ratio, BR = buying ratio
|
||||
//@function Calculates BRAR (BR and AR) sentiment oscillator
|
||||
//@param period Rolling window length for summation (default 26)
|
||||
//@returns tuple [br, ar] where BR measures buying pressure vs previous close,
|
||||
// AR measures selling pressure vs today's open. Both scaled x100.
|
||||
//@optimized Uses 4 circular buffers for O(1) per-bar complexity
|
||||
brar(simple int period) =>
|
||||
if period <= 0
|
||||
runtime.error("Period must be greater than 0")
|
||||
|
||||
var int p = math.max(1, period)
|
||||
var int head = 0
|
||||
var int count = 0
|
||||
|
||||
// Four circular buffers for running sums
|
||||
var array<float> arNumBuf = array.new_float(p, na) // HIGH - OPEN
|
||||
var array<float> arDenBuf = array.new_float(p, na) // OPEN - LOW
|
||||
var array<float> brNumBuf = array.new_float(p, na) // max(0, HIGH - prevClose)
|
||||
var array<float> brDenBuf = array.new_float(p, na) // max(0, prevClose - LOW)
|
||||
|
||||
var float arNumSum = 0.0
|
||||
var float arDenSum = 0.0
|
||||
var float brNumSum = 0.0
|
||||
var float brDenSum = 0.0
|
||||
if period > 5000
|
||||
runtime.error("Period exceeds maximum of 5000")
|
||||
|
||||
float prevClose = nz(close[1], open)
|
||||
|
||||
// Current bar components
|
||||
float arNum = high - open
|
||||
float arDen = open - low
|
||||
float brNum = math.max(0.0, high - prevClose)
|
||||
float brDen = math.max(0.0, prevClose - low)
|
||||
float arNum = math.max(0.0, high - open)
|
||||
float arDen = math.max(0.0, open - low)
|
||||
|
||||
// Remove oldest values from running sums
|
||||
float oldArNum = array.get(arNumBuf, head)
|
||||
float oldArDen = array.get(arDenBuf, head)
|
||||
float oldBrNum = array.get(brNumBuf, head)
|
||||
float oldBrDen = array.get(brDenBuf, head)
|
||||
var array<float> brNumBuf = array.new_float(period, 0.0)
|
||||
var array<float> brDenBuf = array.new_float(period, 0.0)
|
||||
var array<float> arNumBuf = array.new_float(period, 0.0)
|
||||
var array<float> arDenBuf = array.new_float(period, 0.0)
|
||||
var int idx = 0
|
||||
var float brNumSum = 0.0
|
||||
var float brDenSum = 0.0
|
||||
var float arNumSum = 0.0
|
||||
var float arDenSum = 0.0
|
||||
|
||||
if not na(oldArNum)
|
||||
arNumSum -= oldArNum
|
||||
arDenSum -= oldArDen
|
||||
brNumSum -= oldBrNum
|
||||
brDenSum -= oldBrDen
|
||||
else
|
||||
count := math.min(count + 1, p)
|
||||
brNumSum -= array.get(brNumBuf, idx)
|
||||
brDenSum -= array.get(brDenBuf, idx)
|
||||
arNumSum -= array.get(arNumBuf, idx)
|
||||
arDenSum -= array.get(arDenBuf, idx)
|
||||
|
||||
array.set(brNumBuf, idx, brNum)
|
||||
array.set(brDenBuf, idx, brDen)
|
||||
array.set(arNumBuf, idx, arNum)
|
||||
array.set(arDenBuf, idx, arDen)
|
||||
|
||||
// Add current values to running sums
|
||||
arNumSum += arNum
|
||||
arDenSum += arDen
|
||||
brNumSum += brNum
|
||||
brDenSum += brDen
|
||||
arNumSum += arNum
|
||||
arDenSum += arDen
|
||||
|
||||
// Store in circular buffers
|
||||
array.set(arNumBuf, head, arNum)
|
||||
array.set(arDenBuf, head, arDen)
|
||||
array.set(brNumBuf, head, brNum)
|
||||
array.set(brDenBuf, head, brDen)
|
||||
head := (head + 1) % p
|
||||
idx := (idx + 1) % period
|
||||
|
||||
// Calculate AR and BR ratios (* 100)
|
||||
float ar = arDenSum != 0.0 ? (arNumSum / arDenSum) * 100.0 : 0.0
|
||||
float br = brDenSum != 0.0 ? (brNumSum / brDenSum) * 100.0 : 0.0
|
||||
float br = brDenSum != 0.0 ? brNumSum / brDenSum * 100.0 : 100.0
|
||||
float ar = arDenSum != 0.0 ? arNumSum / arDenSum * 100.0 : 100.0
|
||||
|
||||
[ar, br]
|
||||
[br, ar]
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_period = input.int(26, "Period", minval=1, maxval=500)
|
||||
i_period = input.int(26, "Period", minval=1, maxval=5000, tooltip="Rolling window for summation (traditional: 26)")
|
||||
|
||||
// Calculation
|
||||
[ar_value, br_value] = brar(i_period)
|
||||
[br_value, ar_value] = brar(i_period)
|
||||
|
||||
// Plot
|
||||
plot(ar_value, "AR", color.new(color.yellow, 0), 2)
|
||||
plot(br_value, "BR", color.new(color.aqua, 0), 2)
|
||||
hline(100, "Reference", color=color.gray, linestyle=hline.style_dotted)
|
||||
plot(ar_value, "AR", color.new(color.yellow, 0), 2)
|
||||
hline(100, "Equilibrium", color=color.gray, linestyle=hline.style_dotted)
|
||||
|
||||
Reference in New Issue
Block a user