mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-21 03:58:04 +00:00
adding missing validations
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class BbiIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void BbiIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new BbiIndicator();
|
||||
|
||||
Assert.Equal(3, indicator.Period1);
|
||||
Assert.Equal(6, indicator.Period2);
|
||||
Assert.Equal(12, indicator.Period3);
|
||||
Assert.Equal(24, indicator.Period4);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("BBI - Bulls Bears Index", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbiIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new BbiIndicator();
|
||||
|
||||
Assert.Equal(0, BbiIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbiIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new BbiIndicator { Period1 = 3, Period2 = 6, Period3 = 12, Period4 = 24 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("BBI", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("3", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("24", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbiIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new BbiIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Bbi", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbiIndicator_Initialize_CreatesOneLineSeries()
|
||||
{
|
||||
var indicator = new BbiIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbiIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new BbiIndicator { Period1 = 3, Period2 = 6, Period3 = 12, Period4 = 24 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; 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 bbi = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(bbi));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbiIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new BbiIndicator { Period1 = 3, Period2 = 6, Period3 = 12, Period4 = 24 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 25; 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(25), 125, 135, 115, 130);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double bbi = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(bbi));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbiIndicator_DifferentSourceTypes_ProcessCorrectly()
|
||||
{
|
||||
foreach (var sourceType in new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close })
|
||||
{
|
||||
var indicator = new BbiIndicator
|
||||
{
|
||||
Period1 = 3,
|
||||
Period2 = 6,
|
||||
Period3 = 12,
|
||||
Period4 = 24,
|
||||
Source = sourceType
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i * 0.5, 110 + i * 0.5, 90 + i * 0.5, 105 + i * 0.5);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbiIndicator_CustomPeriods_SetsNameCorrectly()
|
||||
{
|
||||
var indicator = new BbiIndicator { Period1 = 5, Period2 = 10, Period3 = 20, Period4 = 40 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("5", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("40", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class BbiIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period 1 (Ultra-Short)", sortIndex: 1, 1, 5000, 1, 0)]
|
||||
public int Period1 { get; set; } = 3;
|
||||
|
||||
[InputParameter("Period 2 (Short)", sortIndex: 2, 1, 5000, 1, 0)]
|
||||
public int Period2 { get; set; } = 6;
|
||||
|
||||
[InputParameter("Period 3 (Medium)", sortIndex: 3, 1, 5000, 1, 0)]
|
||||
public int Period3 { get; set; } = 12;
|
||||
|
||||
[InputParameter("Period 4 (Long)", sortIndex: 4, 1, 5000, 1, 0)]
|
||||
public int Period4 { get; set; } = 24;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput(sortIndex: 5)]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Bbi _bbi = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"BBI ({Period1},{Period2},{Period3},{Period4})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/bbi/Bbi.Quantower.cs";
|
||||
|
||||
public BbiIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "BBI - Bulls Bears Index";
|
||||
Description = "Arithmetic mean of four SMAs across geometrically spaced periods";
|
||||
|
||||
_series = new LineSeries("BBI", Color.Yellow, 2, LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_bbi = new Bbi(Period1, Period2, Period3, Period4);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var priceSelector = Source.GetPriceSelector();
|
||||
var item = HistoricalData[0, SeekOriginHistory.End];
|
||||
double price = priceSelector(item);
|
||||
|
||||
TValue input = new(item.TimeLeft, price);
|
||||
TValue result = _bbi.Update(input, args.IsNewBar());
|
||||
|
||||
if (!_bbi.IsHot && !ShowColdValues)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_series.SetValue(result.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class BbiTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
// ───── A) Constructor validation ─────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_P1Zero_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Bbi(p1: 0));
|
||||
Assert.Equal("p1", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_P2Negative_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Bbi(p2: -1));
|
||||
Assert.Equal("p2", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_P3Zero_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Bbi(p3: 0));
|
||||
Assert.Equal("p3", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_P4Negative_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Bbi(p4: -5));
|
||||
Assert.Equal("p4", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Defaults_SetsProperties()
|
||||
{
|
||||
var bbi = new Bbi();
|
||||
Assert.Equal("Bbi(3,6,12,24)", bbi.Name);
|
||||
Assert.Equal(24, bbi.WarmupPeriod);
|
||||
Assert.False(bbi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomParams_SetsName()
|
||||
{
|
||||
var bbi = new Bbi(p1: 5, p2: 10, p3: 20, p4: 40);
|
||||
Assert.Equal("Bbi(5,10,20,40)", bbi.Name);
|
||||
Assert.Equal(40, bbi.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WarmupPeriod_IsMaxPeriod()
|
||||
{
|
||||
var bbi = new Bbi(p1: 2, p2: 7, p3: 14, p4: 30);
|
||||
Assert.Equal(30, bbi.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ───── B) Basic calculation ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var bbi = new Bbi();
|
||||
var result = bbi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.IsType<TValue>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Last_IsAccessible()
|
||||
{
|
||||
var bbi = new Bbi();
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(default, bbi.Last);
|
||||
Assert.False(bbi.IsHot);
|
||||
Assert.Equal("Bbi(3,6,12,24)", bbi.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantInput_BbiEqualsConstant()
|
||||
{
|
||||
// When all values are constant, every SMA == constant, so BBI == constant.
|
||||
var bbi = new Bbi(p1: 3, p2: 6, p3: 12, p4: 24);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 50.0));
|
||||
}
|
||||
Assert.Equal(50.0, bbi.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SingleBar_ValueIsFinite()
|
||||
{
|
||||
var bbi = new Bbi();
|
||||
var result = bbi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_KnownValue_FirstBar()
|
||||
{
|
||||
// With 1 bar at 100.0, all 4 SMAs = 100.0 → BBI = 100.0
|
||||
var bbi = new Bbi(p1: 3, p2: 6, p3: 12, p4: 24);
|
||||
var result = bbi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(100.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
// ───── C) State + bar correction ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_True_AdvancesState()
|
||||
{
|
||||
var bbi = new Bbi();
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 110.0), isNew: true);
|
||||
Assert.NotEqual(default, bbi.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_False_RollsBack()
|
||||
{
|
||||
var bbi = new Bbi(p1: 3, p2: 6, p3: 12, p4: 24);
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 100.0 + i), isNew: true);
|
||||
}
|
||||
|
||||
// Bar correction: rewrite last bar
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 105.0), isNew: false);
|
||||
double corrected1 = bbi.Last.Value;
|
||||
|
||||
// Same correction again — must produce identical result
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 105.0), isNew: false);
|
||||
double corrected2 = bbi.Last.Value;
|
||||
|
||||
Assert.Equal(corrected1, corrected2, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_RestoreBaseline()
|
||||
{
|
||||
var bbi = new Bbi(p1: 3, p2: 6, p3: 12, p4: 24);
|
||||
double[] data = [100, 102, 104, 106, 108, 110, 112, 114, 116, 118,
|
||||
120, 122, 124, 126, 128, 130, 132, 134, 136, 138,
|
||||
140, 142, 144, 146, 148];
|
||||
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
bbi.Update(new TValue(DateTime.UtcNow, data[i]), isNew: true);
|
||||
}
|
||||
double baseline = bbi.Last.Value;
|
||||
|
||||
// Correct last bar several times, restore original
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 999.0), isNew: false);
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 888.0), isNew: false);
|
||||
bbi.Update(new TValue(DateTime.UtcNow, data[^1]), isNew: false);
|
||||
|
||||
Assert.Equal(baseline, bbi.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var bbi = new Bbi();
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
Assert.True(bbi.IsHot);
|
||||
|
||||
bbi.Reset();
|
||||
Assert.False(bbi.IsHot);
|
||||
Assert.Equal(default, bbi.Last);
|
||||
}
|
||||
|
||||
// ───── D) Warmup/convergence ─────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsAtWarmupPeriod()
|
||||
{
|
||||
var bbi = new Bbi(p1: 3, p2: 6, p3: 12, p4: 24);
|
||||
// IsHot = (index >= WarmupPeriod) = (index >= 24)
|
||||
for (int i = 0; i < 23; i++)
|
||||
{
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
Assert.False(bbi.IsHot);
|
||||
}
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.True(bbi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_EqualsMaxPeriod()
|
||||
{
|
||||
var bbi = new Bbi(p1: 3, p2: 6, p3: 12, p4: 24);
|
||||
Assert.Equal(24, bbi.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ───── E) Robustness ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValid()
|
||||
{
|
||||
var bbi = new Bbi(p1: 3, p2: 6, p3: 12, p4: 24);
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
bbi.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsFinite(bbi.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PositiveInfinity_UsesLastValid()
|
||||
{
|
||||
var bbi = new Bbi();
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
bbi.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(bbi.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NegativeInfinity_UsesLastValid()
|
||||
{
|
||||
var bbi = new Bbi();
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
bbi.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(bbi.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BatchNaN_Safe()
|
||||
{
|
||||
var bbi = new Bbi();
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
bbi.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
}
|
||||
Assert.True(double.IsFinite(bbi.Last.Value));
|
||||
}
|
||||
|
||||
// ───── F) Consistency (streaming == batch TSeries == batch Span == eventing) ─────
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResults()
|
||||
{
|
||||
int p1 = 3, p2 = 6, p3 = 12, p4 = 24;
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
// 1. Streaming
|
||||
var streaming = new Bbi(p1, p2, p3, p4);
|
||||
var streamResults = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(source[i]).Value;
|
||||
}
|
||||
|
||||
// 2. Batch TSeries
|
||||
TSeries batchSeries = Bbi.Batch(source, p1, p2, p3, p4);
|
||||
|
||||
// 3. Batch Span
|
||||
var spanOutput = new double[source.Count];
|
||||
Bbi.Batch(source.Values, spanOutput, p1, p2, p3, p4);
|
||||
|
||||
// 4. Event-based
|
||||
var eventSource = new TSeries();
|
||||
var eventIndicator = new Bbi(eventSource, p1, p2, p3, p4);
|
||||
var eventResults = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
eventSource.Add(source[i]);
|
||||
eventResults[i] = eventIndicator.Last.Value;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchSeries.Values[i], Tolerance);
|
||||
Assert.Equal(streamResults[i], spanOutput[i], Tolerance);
|
||||
Assert.Equal(streamResults[i], eventResults[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
// ───── G) Span API tests ─────
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_MismatchedLength_ThrowsArgumentException()
|
||||
{
|
||||
var source = new double[10];
|
||||
var output = new double[5];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Bbi.Batch(source.AsSpan(), output.AsSpan()));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_ZeroP1_ThrowsArgumentException()
|
||||
{
|
||||
var source = new double[10];
|
||||
var output = new double[10];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Bbi.Batch(source.AsSpan(), output.AsSpan(), p1: 0));
|
||||
Assert.Equal("p1", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_ZeroP2_ThrowsArgumentException()
|
||||
{
|
||||
var source = new double[10];
|
||||
var output = new double[10];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Bbi.Batch(source.AsSpan(), output.AsSpan(), p2: 0));
|
||||
Assert.Equal("p2", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_ZeroP3_ThrowsArgumentException()
|
||||
{
|
||||
var source = new double[10];
|
||||
var output = new double[10];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Bbi.Batch(source.AsSpan(), output.AsSpan(), p3: 0));
|
||||
Assert.Equal("p3", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_ZeroP4_ThrowsArgumentException()
|
||||
{
|
||||
var source = new double[10];
|
||||
var output = new double[10];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Bbi.Batch(source.AsSpan(), output.AsSpan(), p4: 0));
|
||||
Assert.Equal("p4", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_Empty_NoException()
|
||||
{
|
||||
double[] source = [];
|
||||
double[] output = [];
|
||||
var ex = Record.Exception(() => Bbi.Batch(source.AsSpan(), output.AsSpan()));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_MatchesTSeries()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 7);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
TSeries batchTs = Bbi.Batch(source);
|
||||
var spanOutput = new double[source.Count];
|
||||
Bbi.Batch(source.Values, spanOutput);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchTs.Values[i], spanOutput[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_NaN_Handled()
|
||||
{
|
||||
double[] src = [1, 2, double.NaN, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25];
|
||||
var output = new double[src.Length];
|
||||
var ex = Record.Exception(() => Bbi.Batch(src.AsSpan(), output.AsSpan()));
|
||||
Assert.Null(ex);
|
||||
Assert.All(output, v => Assert.True(double.IsFinite(v)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_LargeInput_NoStackOverflow()
|
||||
{
|
||||
int n = 10_000;
|
||||
var source = new double[n];
|
||||
var output = new double[n];
|
||||
for (int i = 0; i < n; i++) { source[i] = 100.0 + i * 0.01; }
|
||||
var ex = Record.Exception(() => Bbi.Batch(source.AsSpan(), output.AsSpan()));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
// ───── H) Chainability ─────
|
||||
|
||||
[Fact]
|
||||
public void PubEvent_FiresOnUpdate()
|
||||
{
|
||||
var bbi = new Bbi();
|
||||
int firedCount = 0;
|
||||
bbi.Pub += (object? _, in TValueEventArgs _) => firedCount++;
|
||||
|
||||
bbi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(1, firedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventChaining_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var bbi = new Bbi(source);
|
||||
var downstream = new TSeries();
|
||||
bbi.Pub += (object? _, in TValueEventArgs e) => downstream.Add(e.Value);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
|
||||
Assert.Equal(30, downstream.Count);
|
||||
}
|
||||
|
||||
// ───── Calculate ─────
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultsAndHotIndicator()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
var (results, indicator) = Bbi.Calculate(source);
|
||||
|
||||
Assert.Equal(source.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
// ───── Update(TSeries) ─────
|
||||
|
||||
[Fact]
|
||||
public void UpdateTSeries_MatchesStreaming()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
var streaming = new Bbi();
|
||||
var streamResults = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(source[i]).Value;
|
||||
}
|
||||
|
||||
var batch = new Bbi();
|
||||
TSeries batchResults = batch.Update(source);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i], Tolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Self-consistency validation: batch TSeries == streaming == span == eventing.
|
||||
/// No external library implements BBI, so cross-library comparison is N/A.
|
||||
/// </summary>
|
||||
public sealed class BbiValidationTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
private static TSeries BuildGbmSeries(int count, int seed = 1)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: seed);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
return bars.Close;
|
||||
}
|
||||
|
||||
// ── Batch == Streaming ───────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Batch_EqualsStreaming_DefaultPeriods()
|
||||
{
|
||||
TSeries source = BuildGbmSeries(500, seed: 1);
|
||||
|
||||
// Streaming
|
||||
var bbi = new Bbi();
|
||||
var streamVals = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streamVals[i] = bbi.Update(source[i]).Value;
|
||||
}
|
||||
|
||||
// Batch TSeries
|
||||
TSeries batch = Bbi.Batch(source);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamVals[i], batch.Values[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EqualsStreaming_CustomPeriods()
|
||||
{
|
||||
TSeries source = BuildGbmSeries(300, seed: 2);
|
||||
int p1 = 5, p2 = 10, p3 = 20, p4 = 40;
|
||||
|
||||
var bbi = new Bbi(p1, p2, p3, p4);
|
||||
var streamVals = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streamVals[i] = bbi.Update(source[i]).Value;
|
||||
}
|
||||
|
||||
TSeries batch = Bbi.Batch(source, p1, p2, p3, p4);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamVals[i], batch.Values[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Batch(Span) == Batch(TSeries) ────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_EqualsBatchTSeries_DefaultPeriods()
|
||||
{
|
||||
TSeries source = BuildGbmSeries(400, seed: 3);
|
||||
|
||||
TSeries batchTs = Bbi.Batch(source);
|
||||
var spanOut = new double[source.Count];
|
||||
Bbi.Batch(source.Values, spanOut);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchTs.Values[i], spanOut[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_EqualsBatchTSeries_CustomPeriods()
|
||||
{
|
||||
TSeries source = BuildGbmSeries(200, seed: 4);
|
||||
int p1 = 4, p2 = 8, p3 = 16, p4 = 32;
|
||||
|
||||
TSeries batchTs = Bbi.Batch(source, p1, p2, p3, p4);
|
||||
var spanOut = new double[source.Count];
|
||||
Bbi.Batch(source.Values, spanOut, p1, p2, p3, p4);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchTs.Values[i], spanOut[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Eventing == Streaming ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Eventing_EqualsStreaming_DefaultPeriods()
|
||||
{
|
||||
TSeries source = BuildGbmSeries(300, seed: 5);
|
||||
|
||||
// Streaming
|
||||
var bbi = new Bbi();
|
||||
var streamVals = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streamVals[i] = bbi.Update(source[i]).Value;
|
||||
}
|
||||
|
||||
// Eventing
|
||||
var eventSource = new TSeries();
|
||||
var eventBbi = new Bbi(eventSource);
|
||||
var eventVals = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
eventSource.Add(source[i]);
|
||||
eventVals[i] = eventBbi.Last.Value;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamVals[i], eventVals[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mathematical properties ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_BbiEqualsConstant()
|
||||
{
|
||||
// Constant price → all SMAs == price → BBI == price
|
||||
const double price = 75.0;
|
||||
var bbi = new Bbi();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
bbi.Update(new TValue(DateTime.UtcNow, price));
|
||||
}
|
||||
Assert.Equal(price, bbi.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_BatchBbiEqualsConstant()
|
||||
{
|
||||
const double price = 125.0;
|
||||
int n = 100;
|
||||
var source = new double[n];
|
||||
var output = new double[n];
|
||||
for (int i = 0; i < n; i++) { source[i] = price; }
|
||||
Bbi.Batch(source.AsSpan(), output.AsSpan());
|
||||
|
||||
// After full warmup (bar 24+), every output should equal price
|
||||
for (int i = 24; i < n; i++)
|
||||
{
|
||||
Assert.Equal(price, output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllPeriodsOne_BbiEqualsInput()
|
||||
{
|
||||
// With all periods=1, each SMA is just the current value → BBI == current value
|
||||
var bbi = new Bbi(p1: 1, p2: 1, p3: 1, p4: 1);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 6);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var result = bbi.Update(source[i]);
|
||||
Assert.Equal(source.Values[i], result.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
// ── UpdateTSeries primes streaming state correctly ───────────────────────
|
||||
|
||||
[Fact]
|
||||
public void UpdateTSeries_ContinuedStreaming_Consistent()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 7);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
// Instance-based batch (internally resets + re-streams)
|
||||
var batchBbi = new Bbi();
|
||||
batchBbi.Update(source);
|
||||
|
||||
// Pure streaming
|
||||
var streamBbi = new Bbi();
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streamBbi.Update(source[i]);
|
||||
}
|
||||
|
||||
// Both should have identical Last values after processing same data
|
||||
Assert.Equal(streamBbi.Last.Value, batchBbi.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
// ── Calculate bridge ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ResultsMatchBatch()
|
||||
{
|
||||
TSeries source = BuildGbmSeries(200, seed: 8);
|
||||
|
||||
var (results, indicator) = Bbi.Calculate(source);
|
||||
TSeries batch = Bbi.Batch(source);
|
||||
|
||||
Assert.Equal(batch.Count, results.Count);
|
||||
for (int i = 0; i < batch.Count; i++)
|
||||
{
|
||||
Assert.Equal(batch.Values[i], results.Values[i], Tolerance);
|
||||
}
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
// BBI: Bulls Bears Index
|
||||
// Average of four SMAs with geometrically spaced periods (default 3, 6, 12, 24).
|
||||
// Formula: BBI = (SMA(p1) + SMA(p2) + SMA(p3) + SMA(p4)) / 4
|
||||
// Origin: Chinese technical analysis community.
|
||||
// Source: bbi.pine
|
||||
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// BBI: Bulls Bears Index
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Computes the arithmetic mean of four independent Simple Moving Averages with
|
||||
/// geometrically spaced periods (default 3, 6, 12, 24). The composite line captures
|
||||
/// trend consensus across ultra-short, short, medium, and long timeframes simultaneously.
|
||||
/// Price above BBI signals bullish regime; price below BBI signals bearish regime.
|
||||
///
|
||||
/// Calculation (O(1) per bar via four independent circular-buffer SMAs):
|
||||
/// BBI = (SMA(src, p1) + SMA(src, p2) + SMA(src, p3) + SMA(src, p4)) / 4
|
||||
///
|
||||
/// Default parameters: p1=3, p2=6, p3=12, p4=24
|
||||
/// WarmupPeriod = max(p1, p2, p3, p4)
|
||||
///
|
||||
/// Sources:
|
||||
/// - Chinese Securities Association technical analysis specifications
|
||||
/// - TradingView community: "BBI - Bull and Bear Index"
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Bbi : AbstractBase
|
||||
{
|
||||
private const int DefaultP1 = 3;
|
||||
private const int DefaultP2 = 6;
|
||||
private const int DefaultP3 = 12;
|
||||
private const int DefaultP4 = 24;
|
||||
|
||||
private readonly int _p1, _p2, _p3, _p4;
|
||||
|
||||
// Four independent O(1) circular-buffer SMAs
|
||||
private readonly double[] _buf1, _buf2, _buf3, _buf4;
|
||||
|
||||
// All scalar state in one record struct for atomic _ps=_s snapshot (bar correction).
|
||||
// PrevSlotX = the value that was at buf[headX] BEFORE the most recent isNew=true write.
|
||||
// On isNew=false, restore buf[_ps.HeadX] = _s.PrevSlotX, then _s = _ps.
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double Sum1, int Head1, int Count1, double PrevSlot1,
|
||||
double Sum2, int Head2, int Count2, double PrevSlot2,
|
||||
double Sum3, int Head3, int Count3, double PrevSlot3,
|
||||
double Sum4, int Head4, int Count4, double PrevSlot4,
|
||||
int Index, double LastValid);
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <summary>
|
||||
/// Creates BBI with four customizable SMA periods.
|
||||
/// </summary>
|
||||
/// <param name="p1">Ultra-short SMA period (must be > 0)</param>
|
||||
/// <param name="p2">Short SMA period (must be > 0)</param>
|
||||
/// <param name="p3">Medium SMA period (must be > 0)</param>
|
||||
/// <param name="p4">Long SMA period (must be > 0)</param>
|
||||
public Bbi(int p1 = DefaultP1, int p2 = DefaultP2, int p3 = DefaultP3, int p4 = DefaultP4)
|
||||
{
|
||||
if (p1 <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period 1 must be greater than 0", nameof(p1));
|
||||
}
|
||||
if (p2 <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period 2 must be greater than 0", nameof(p2));
|
||||
}
|
||||
if (p3 <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period 3 must be greater than 0", nameof(p3));
|
||||
}
|
||||
if (p4 <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period 4 must be greater than 0", nameof(p4));
|
||||
}
|
||||
|
||||
_p1 = p1; _p2 = p2; _p3 = p3; _p4 = p4;
|
||||
|
||||
_buf1 = new double[p1];
|
||||
_buf2 = new double[p2];
|
||||
_buf3 = new double[p3];
|
||||
_buf4 = new double[p4];
|
||||
|
||||
WarmupPeriod = Math.Max(Math.Max(p1, p2), Math.Max(p3, p4));
|
||||
Name = $"Bbi({p1},{p2},{p3},{p4})";
|
||||
_s = default;
|
||||
_ps = _s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates BBI subscribed to a source publisher.
|
||||
/// </summary>
|
||||
public Bbi(ITValuePublisher source,
|
||||
int p1 = DefaultP1, int p2 = DefaultP2, int p3 = DefaultP3, int p4 = DefaultP4)
|
||||
: this(p1, p2, p3, p4)
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>True when enough bars have been processed for valid (full-window) output.</summary>
|
||||
public override bool IsHot => _s.Index >= WarmupPeriod;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore the ring-buffer slots that were overwritten by the most-recent isNew=true pass.
|
||||
// _ps.HeadX = the write-head position used during that pass.
|
||||
// _s.PrevSlotX = the value that was at that head BEFORE the write.
|
||||
_buf1[_ps.Head1] = _s.PrevSlot1;
|
||||
_buf2[_ps.Head2] = _s.PrevSlot2;
|
||||
_buf3[_ps.Head3] = _s.PrevSlot3;
|
||||
_buf4[_ps.Head4] = _s.PrevSlot4;
|
||||
_s = _ps;
|
||||
}
|
||||
|
||||
// Local copy for JIT register promotion
|
||||
double sum1 = _s.Sum1; int h1 = _s.Head1; int c1 = _s.Count1;
|
||||
double sum2 = _s.Sum2; int h2 = _s.Head2; int c2 = _s.Count2;
|
||||
double sum3 = _s.Sum3; int h3 = _s.Head3; int c3 = _s.Count3;
|
||||
double sum4 = _s.Sum4; int h4 = _s.Head4; int c4 = _s.Count4;
|
||||
int index = _s.Index;
|
||||
double lastValid = _s.LastValid;
|
||||
|
||||
// NaN/Infinity substitution
|
||||
double val = input.Value;
|
||||
if (!double.IsFinite(val))
|
||||
{
|
||||
val = double.IsFinite(lastValid) ? lastValid : 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastValid = val;
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
// ── SMA 1: capture slot BEFORE writing (for bar-correction restore next time) ──
|
||||
double prev1 = _buf1[h1];
|
||||
sum1 = c1 < _p1 ? sum1 + val - prev1 : sum1 - prev1 + val;
|
||||
if (c1 < _p1) { c1++; }
|
||||
_buf1[h1] = val;
|
||||
int newH1 = isNew ? (h1 + 1) % _p1 : h1;
|
||||
|
||||
// ── SMA 2 ────────────────────────────────────────────────────────────
|
||||
double prev2 = _buf2[h2];
|
||||
sum2 = c2 < _p2 ? sum2 + val - prev2 : sum2 - prev2 + val;
|
||||
if (c2 < _p2) { c2++; }
|
||||
_buf2[h2] = val;
|
||||
int newH2 = isNew ? (h2 + 1) % _p2 : h2;
|
||||
|
||||
// ── SMA 3 ────────────────────────────────────────────────────────────
|
||||
double prev3 = _buf3[h3];
|
||||
sum3 = c3 < _p3 ? sum3 + val - prev3 : sum3 - prev3 + val;
|
||||
if (c3 < _p3) { c3++; }
|
||||
_buf3[h3] = val;
|
||||
int newH3 = isNew ? (h3 + 1) % _p3 : h3;
|
||||
|
||||
// ── SMA 4 ────────────────────────────────────────────────────────────
|
||||
double prev4 = _buf4[h4];
|
||||
sum4 = c4 < _p4 ? sum4 + val - prev4 : sum4 - prev4 + val;
|
||||
if (c4 < _p4) { c4++; }
|
||||
_buf4[h4] = val;
|
||||
int newH4 = isNew ? (h4 + 1) % _p4 : h4;
|
||||
|
||||
// ── Composite BBI ────────────────────────────────────────────────────
|
||||
double sma1 = sum1 / Math.Max(1, c1);
|
||||
double sma2 = sum2 / Math.Max(1, c2);
|
||||
double sma3 = sum3 / Math.Max(1, c3);
|
||||
double sma4 = sum4 / Math.Max(1, c4);
|
||||
double bbi = (sma1 + sma2 + sma3 + sma4) * 0.25;
|
||||
|
||||
// Write back state — store PrevSlotX for next bar-correction restore
|
||||
_s = new State(
|
||||
sum1, newH1, c1, prev1,
|
||||
sum2, newH2, c2, prev2,
|
||||
sum3, newH3, c3, prev3,
|
||||
sum4, newH4, c4, prev4,
|
||||
index, lastValid);
|
||||
|
||||
Last = new TValue(input.Time, bbi);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Batch(source.Values, vSpan, _p1, _p2, _p3, _p4);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Prime streaming state for continued updates
|
||||
Reset();
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, source[i]), isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Reset()
|
||||
{
|
||||
Array.Clear(_buf1);
|
||||
Array.Clear(_buf2);
|
||||
Array.Clear(_buf3);
|
||||
Array.Clear(_buf4);
|
||||
_s = default;
|
||||
_ps = _s;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
// ── Static Batch (TSeries) ───────────────────────────────────────────────
|
||||
|
||||
/// <summary>Calculates BBI for an entire <see cref="TSeries"/>.</summary>
|
||||
public static TSeries Batch(
|
||||
TSeries source,
|
||||
int p1 = DefaultP1, int p2 = DefaultP2, int p3 = DefaultP3, int p4 = DefaultP4)
|
||||
{
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Batch(source.Values, vSpan, p1, p2, p3, p4);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
// ── Static Batch (Span) ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Zero-allocation span-based BBI calculation using ArrayPool for ring buffers.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> source,
|
||||
Span<double> output,
|
||||
int p1 = DefaultP1, int p2 = DefaultP2, int p3 = DefaultP3, int p4 = DefaultP4)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
||||
}
|
||||
if (p1 <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period 1 must be greater than 0", nameof(p1));
|
||||
}
|
||||
if (p2 <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period 2 must be greater than 0", nameof(p2));
|
||||
}
|
||||
if (p3 <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period 3 must be greater than 0", nameof(p3));
|
||||
}
|
||||
if (p4 <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period 4 must be greater than 0", nameof(p4));
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double[] b1 = ArrayPool<double>.Shared.Rent(p1);
|
||||
double[] b2 = ArrayPool<double>.Shared.Rent(p2);
|
||||
double[] b3 = ArrayPool<double>.Shared.Rent(p3);
|
||||
double[] b4 = ArrayPool<double>.Shared.Rent(p4);
|
||||
|
||||
b1.AsSpan(0, p1).Clear();
|
||||
b2.AsSpan(0, p2).Clear();
|
||||
b3.AsSpan(0, p3).Clear();
|
||||
b4.AsSpan(0, p4).Clear();
|
||||
|
||||
try
|
||||
{
|
||||
double sum1 = 0, sum2 = 0, sum3 = 0, sum4 = 0;
|
||||
int h1 = 0, h2 = 0, h3 = 0, h4 = 0;
|
||||
int c1 = 0, c2 = 0, c3 = 0, c4 = 0;
|
||||
double lastValid = 0.0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (!double.IsFinite(val))
|
||||
{
|
||||
val = lastValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastValid = val;
|
||||
}
|
||||
|
||||
double old1 = b1[h1]; sum1 = c1 < p1 ? sum1 + val - old1 : sum1 - old1 + val; if (c1 < p1) { c1++; }
|
||||
b1[h1] = val; h1 = (h1 + 1) % p1;
|
||||
double old2 = b2[h2]; sum2 = c2 < p2 ? sum2 + val - old2 : sum2 - old2 + val; if (c2 < p2) { c2++; }
|
||||
b2[h2] = val; h2 = (h2 + 1) % p2;
|
||||
double old3 = b3[h3]; sum3 = c3 < p3 ? sum3 + val - old3 : sum3 - old3 + val; if (c3 < p3) { c3++; }
|
||||
b3[h3] = val; h3 = (h3 + 1) % p3;
|
||||
double old4 = b4[h4]; sum4 = c4 < p4 ? sum4 + val - old4 : sum4 - old4 + val; if (c4 < p4) { c4++; }
|
||||
b4[h4] = val; h4 = (h4 + 1) % p4;
|
||||
|
||||
output[i] = (sum1 / Math.Max(1, c1) + sum2 / Math.Max(1, c2)
|
||||
+ sum3 / Math.Max(1, c3) + sum4 / Math.Max(1, c4)) * 0.25;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(b1);
|
||||
ArrayPool<double>.Shared.Return(b2);
|
||||
ArrayPool<double>.Shared.Return(b3);
|
||||
ArrayPool<double>.Shared.Return(b4);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates a BBI instance and calculates results for the source series.</summary>
|
||||
public static (TSeries Results, Bbi Indicator) Calculate(
|
||||
TSeries source,
|
||||
int p1 = DefaultP1, int p2 = DefaultP2, int p3 = DefaultP3, int p4 = DefaultP4)
|
||||
{
|
||||
var indicator = new Bbi(p1, p2, p3, p4);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user