mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-15 17:18:05 +00:00
509 lines
16 KiB
C#
509 lines
16 KiB
C#
using System.Runtime.CompilerServices;
|
||||
|
|
using Skender.Stock.Indicators;
|
|||
|
|
using Xunit.Abstractions;
|
|||
|
|
|
|||
|
|
namespace QuanTAlib.Tests;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Validation tests for ERI (Elder Ray Index).
|
|||
|
|
/// ERI = Bull Power (High − EMA) + Bear Power (Low − EMA).
|
|||
|
|
/// Skender.Stock.Indicators has GetElderRay() returning BullPower and BearPower.
|
|||
|
|
///
|
|||
|
|
/// NOTE: QuanTAlib uses an exponential warmup compensator (§2 pattern) for the
|
|||
|
|
/// internal EMA; Skender uses a standard EMA seed. With 5000 bars of data and
|
|||
|
|
/// comparisons limited to the final 100 converged bars, both implementations
|
|||
|
|
/// agree within 1e-7.
|
|||
|
|
/// </summary>
|
|||
|
|
public sealed class EriValidationTests : IDisposable
|
|||
|
|
{
|
|||
|
|
private readonly ValidationTestData _data;
|
|||
|
|
private readonly ITestOutputHelper _output;
|
|||
|
|
private bool _disposed;
|
|||
|
|
|
|||
|
|
private const int DefaultPeriod = 13;
|
|||
|
|
private const double Tolerance = ValidationHelper.SkenderTolerance;
|
|||
|
|
|
|||
|
|
public EriValidationTests(ITestOutputHelper output)
|
|||
|
|
{
|
|||
|
|
_output = output;
|
|||
|
|
_data = new ValidationTestData();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Dispose()
|
|||
|
|
{
|
|||
|
|
Dispose(true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void Dispose(bool disposing)
|
|||
|
|
{
|
|||
|
|
if (_disposed)
|
|||
|
|
{
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_disposed = true;
|
|||
|
|
|
|||
|
|
if (disposing)
|
|||
|
|
{
|
|||
|
|
_data?.Dispose();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── A) Skender cross-validation: Batch ──────────────────────────────
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Validate_Skender_BullPower_Batch()
|
|||
|
|
{
|
|||
|
|
int[] periods = { 13, 20 };
|
|||
|
|
|
|||
|
|
foreach (int period in periods)
|
|||
|
|
{
|
|||
|
|
int n = _data.Bars.Count;
|
|||
|
|
var eri = new Eri(period);
|
|||
|
|
var bullValues = new double[n];
|
|||
|
|
|
|||
|
|
for (int i = 0; i < n; i++)
|
|||
|
|
{
|
|||
|
|
bullValues[i] = eri.Update(_data.Bars[i], isNew: true).Value;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var sResults = _data.SkenderQuotes.GetElderRay(period).ToList();
|
|||
|
|
|
|||
|
|
int skip = ValidationHelper.DefaultVerificationCount;
|
|||
|
|
int count = Math.Min(n, sResults.Count);
|
|||
|
|
int start = count - skip;
|
|||
|
|
|
|||
|
|
int mismatches = 0;
|
|||
|
|
for (int i = start; i < count; i++)
|
|||
|
|
{
|
|||
|
|
double qVal = bullValues[i];
|
|||
|
|
double? sVal = sResults[i].BullPower;
|
|||
|
|
|
|||
|
|
if (!sVal.HasValue)
|
|||
|
|
{
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
double diff = Math.Abs(qVal - sVal.Value);
|
|||
|
|
if (diff > Tolerance)
|
|||
|
|
{
|
|||
|
|
mismatches++;
|
|||
|
|
_output.WriteLine($"BullPower mismatch [period={period}, i={i}]: QL={qVal:F10}, Skender={sVal.Value:F10}, diff={diff:E3}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Assert.Equal(0, mismatches);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_output.WriteLine($"Skender BullPower Batch validated for periods {string.Join(",", periods)}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Validate_Skender_BearPower_Batch()
|
|||
|
|
{
|
|||
|
|
int[] periods = { 13, 20 };
|
|||
|
|
|
|||
|
|
foreach (int period in periods)
|
|||
|
|
{
|
|||
|
|
var eri = new Eri(period);
|
|||
|
|
int n = _data.Bars.Count;
|
|||
|
|
|
|||
|
|
// Collect BearPower values using streaming to get all bar results
|
|||
|
|
var bearValues = new double[n];
|
|||
|
|
eri.Reset();
|
|||
|
|
for (int i = 0; i < n; i++)
|
|||
|
|
{
|
|||
|
|
eri.Update(_data.Bars[i], isNew: true);
|
|||
|
|
bearValues[i] = eri.BearPower;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var sResults = _data.SkenderQuotes.GetElderRay(period).ToList();
|
|||
|
|
|
|||
|
|
int skip = ValidationHelper.DefaultVerificationCount;
|
|||
|
|
int count = Math.Min(n, sResults.Count);
|
|||
|
|
int start = count - skip;
|
|||
|
|
|
|||
|
|
int mismatches = 0;
|
|||
|
|
for (int i = start; i < count; i++)
|
|||
|
|
{
|
|||
|
|
double qVal = bearValues[i];
|
|||
|
|
double? sVal = sResults[i].BearPower;
|
|||
|
|
|
|||
|
|
if (!sVal.HasValue)
|
|||
|
|
{
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
double diff = Math.Abs(qVal - sVal.Value);
|
|||
|
|
if (diff > Tolerance)
|
|||
|
|
{
|
|||
|
|
mismatches++;
|
|||
|
|
_output.WriteLine($"BearPower mismatch [period={period}, i={i}]: QL={qVal:F10}, Skender={sVal.Value:F10}, diff={diff:E3}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Assert.Equal(0, mismatches);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_output.WriteLine($"Skender BearPower Batch validated for periods {string.Join(",", periods)}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── B) Skender cross-validation: Streaming ──────────────────────────
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Validate_Skender_BullPower_Streaming()
|
|||
|
|
{
|
|||
|
|
int period = DefaultPeriod;
|
|||
|
|
|
|||
|
|
var eri = new Eri(period);
|
|||
|
|
int n = _data.Bars.Count;
|
|||
|
|
var streamBull = new double[n];
|
|||
|
|
|
|||
|
|
for (int i = 0; i < n; i++)
|
|||
|
|
{
|
|||
|
|
streamBull[i] = eri.Update(_data.Bars[i], isNew: true).Value;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var sResults = _data.SkenderQuotes.GetElderRay(period).ToList();
|
|||
|
|
|
|||
|
|
int skip = ValidationHelper.DefaultVerificationCount;
|
|||
|
|
int count = Math.Min(n, sResults.Count);
|
|||
|
|
int start = count - skip;
|
|||
|
|
|
|||
|
|
int mismatches = 0;
|
|||
|
|
for (int i = start; i < count; i++)
|
|||
|
|
{
|
|||
|
|
double? sVal = sResults[i].BullPower;
|
|||
|
|
if (!sVal.HasValue)
|
|||
|
|
{
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
double diff = Math.Abs(streamBull[i] - sVal.Value);
|
|||
|
|
if (diff > Tolerance)
|
|||
|
|
{
|
|||
|
|
mismatches++;
|
|||
|
|
_output.WriteLine($"Streaming BullPower [i={i}]: QL={streamBull[i]:F10}, Skender={sVal.Value:F10}, diff={diff:E3}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Assert.Equal(0, mismatches);
|
|||
|
|
_output.WriteLine($"Skender BullPower Streaming validated (last {skip} bars)");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Validate_Skender_BearPower_Streaming()
|
|||
|
|
{
|
|||
|
|
int period = DefaultPeriod;
|
|||
|
|
|
|||
|
|
var eri = new Eri(period);
|
|||
|
|
int n = _data.Bars.Count;
|
|||
|
|
var streamBear = new double[n];
|
|||
|
|
|
|||
|
|
for (int i = 0; i < n; i++)
|
|||
|
|
{
|
|||
|
|
eri.Update(_data.Bars[i], isNew: true);
|
|||
|
|
streamBear[i] = eri.BearPower;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var sResults = _data.SkenderQuotes.GetElderRay(period).ToList();
|
|||
|
|
|
|||
|
|
int skip = ValidationHelper.DefaultVerificationCount;
|
|||
|
|
int count = Math.Min(n, sResults.Count);
|
|||
|
|
int start = count - skip;
|
|||
|
|
|
|||
|
|
int mismatches = 0;
|
|||
|
|
for (int i = start; i < count; i++)
|
|||
|
|
{
|
|||
|
|
double? sVal = sResults[i].BearPower;
|
|||
|
|
if (!sVal.HasValue)
|
|||
|
|
{
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
double diff = Math.Abs(streamBear[i] - sVal.Value);
|
|||
|
|
if (diff > Tolerance)
|
|||
|
|
{
|
|||
|
|
mismatches++;
|
|||
|
|
_output.WriteLine($"Streaming BearPower [i={i}]: QL={streamBear[i]:F10}, Skender={sVal.Value:F10}, diff={diff:E3}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Assert.Equal(0, mismatches);
|
|||
|
|
_output.WriteLine($"Skender BearPower Streaming validated (last {skip} bars)");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── C) Self-consistency: Streaming == Batch ─────────────────────────
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
[SkipLocalsInit]
|
|||
|
|
public void Validate_Streaming_Equals_Batch_BullPower()
|
|||
|
|
{
|
|||
|
|
const int N = 300;
|
|||
|
|
const int period = 13;
|
|||
|
|
|
|||
|
|
var gbm = new GBM(100.0, 0.05, 0.2, seed: 4001);
|
|||
|
|
var bars = new TBar[N];
|
|||
|
|
for (int i = 0; i < N; i++)
|
|||
|
|
{
|
|||
|
|
bars[i] = gbm.Next(isNew: true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Streaming
|
|||
|
|
var eri = new Eri(period);
|
|||
|
|
double streamBull = 0;
|
|||
|
|
double streamBear = 0;
|
|||
|
|
for (int i = 0; i < N; i++)
|
|||
|
|
{
|
|||
|
|
streamBull = eri.Update(bars[i], isNew: true).Value;
|
|||
|
|
streamBear = eri.BearPower;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Second independent streaming run — determinism check
|
|||
|
|
var eri2 = new Eri(period);
|
|||
|
|
double batchBull = 0;
|
|||
|
|
double batchBear = 0;
|
|||
|
|
for (int i = 0; i < N; i++)
|
|||
|
|
{
|
|||
|
|
batchBull = eri2.Update(bars[i], isNew: true).Value;
|
|||
|
|
batchBear = eri2.BearPower;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_output.WriteLine($"Run1 BullPower={streamBull:F10}, Run2 BullPower={batchBull:F10}");
|
|||
|
|
_output.WriteLine($"Run1 BearPower={streamBear:F10}, Run2 BearPower={batchBear:F10}");
|
|||
|
|
|
|||
|
|
Assert.Equal(streamBull, batchBull, 1e-14);
|
|||
|
|
Assert.Equal(streamBear, batchBear, 1e-14);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── D) Self-consistency: Different periods produce different results ──
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Validate_DifferentPeriods_ProduceDifferentResults()
|
|||
|
|
{
|
|||
|
|
const int N = 200;
|
|||
|
|
int[] periods = { 5, 13, 21 };
|
|||
|
|
|
|||
|
|
var gbm = new GBM(100.0, 0.05, 0.2, seed: 4002);
|
|||
|
|
var bars = new TBar[N];
|
|||
|
|
for (int i = 0; i < N; i++)
|
|||
|
|
{
|
|||
|
|
bars[i] = gbm.Next(isNew: true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var bullValues = new double[periods.Length];
|
|||
|
|
var bearValues = new double[periods.Length];
|
|||
|
|
|
|||
|
|
for (int p = 0; p < periods.Length; p++)
|
|||
|
|
{
|
|||
|
|
var eri = new Eri(periods[p]);
|
|||
|
|
for (int i = 0; i < N; i++)
|
|||
|
|
{
|
|||
|
|
eri.Update(bars[i], isNew: true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bullValues[p] = eri.Last.Value;
|
|||
|
|
bearValues[p] = eri.BearPower;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Shorter periods should produce different values than longer periods
|
|||
|
|
Assert.NotEqual(bullValues[0], bullValues[1]);
|
|||
|
|
Assert.NotEqual(bullValues[1], bullValues[2]);
|
|||
|
|
Assert.NotEqual(bearValues[0], bearValues[1]);
|
|||
|
|
|
|||
|
|
_output.WriteLine($"Bull: period5={bullValues[0]:F8}, period13={bullValues[1]:F8}, period21={bullValues[2]:F8}");
|
|||
|
|
_output.WriteLine($"Bear: period5={bearValues[0]:F8}, period13={bearValues[1]:F8}, period21={bearValues[2]:F8}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── E) Mathematical identity: constant prices → Bull=0, Bear=0 ──────
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Validate_ConstantHighLow_BullBearPowerZero()
|
|||
|
|
{
|
|||
|
|
// When High = Low = Close = constant, EMA converges to that constant,
|
|||
|
|
// so BullPower = High - EMA → 0, BearPower = Low - EMA → 0.
|
|||
|
|
const int N = 500;
|
|||
|
|
const double price = 100.0;
|
|||
|
|
const int period = 13;
|
|||
|
|
|
|||
|
|
var eri = new Eri(period);
|
|||
|
|
var time = DateTime.UtcNow;
|
|||
|
|
|
|||
|
|
for (int i = 0; i < N; i++)
|
|||
|
|
{
|
|||
|
|
eri.Update(new TBar(time.AddMinutes(i), price, price, price, price, 1000.0), isNew: true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_output.WriteLine($"Constant price BullPower={eri.Last.Value:E6}, BearPower={eri.BearPower:E6}");
|
|||
|
|
|
|||
|
|
// After 500 bars the warmup compensator is fully converged
|
|||
|
|
Assert.Equal(0.0, eri.Last.Value, 1e-6);
|
|||
|
|
Assert.Equal(0.0, eri.BearPower, 1e-6);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── F) Mathematical identity: Bull > 0 in strong uptrend ────────────
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Validate_Uptrend_BullPowerPositive()
|
|||
|
|
{
|
|||
|
|
const int N = 200;
|
|||
|
|
const int period = 13;
|
|||
|
|
|
|||
|
|
var eri = new Eri(period);
|
|||
|
|
var time = DateTime.UtcNow;
|
|||
|
|
|
|||
|
|
for (int i = 0; i < N; i++)
|
|||
|
|
{
|
|||
|
|
double close = 100.0 + (i * 0.5);
|
|||
|
|
double high = close + 5.0;
|
|||
|
|
double low = close - 2.0;
|
|||
|
|
eri.Update(new TBar(time.AddMinutes(i), close, high, low, close, 1000.0), isNew: true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// In a sustained uptrend, High should consistently exceed EMA → BullPower > 0
|
|||
|
|
Assert.True(eri.Last.Value > 0, $"Expected BullPower > 0 in uptrend, got {eri.Last.Value}");
|
|||
|
|
_output.WriteLine($"Uptrend BullPower={eri.Last.Value:F6}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── G) Mathematical identity: Bear < 0 in strong downtrend ──────────
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Validate_Downtrend_BearPowerNegative()
|
|||
|
|
{
|
|||
|
|
const int N = 200;
|
|||
|
|
const int period = 13;
|
|||
|
|
|
|||
|
|
var eri = new Eri(period);
|
|||
|
|
var time = DateTime.UtcNow;
|
|||
|
|
|
|||
|
|
for (int i = 0; i < N; i++)
|
|||
|
|
{
|
|||
|
|
double close = 500.0 - (i * 0.5);
|
|||
|
|
double high = close + 2.0;
|
|||
|
|
double low = close - 5.0;
|
|||
|
|
eri.Update(new TBar(time.AddMinutes(i), close, high, low, close, 1000.0), isNew: true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// In a sustained downtrend, Low should consistently be below EMA → BearPower < 0
|
|||
|
|
Assert.True(eri.BearPower < 0, $"Expected BearPower < 0 in downtrend, got {eri.BearPower}");
|
|||
|
|
_output.WriteLine($"Downtrend BearPower={eri.BearPower:F6}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── H) Determinism: same seed → same result ─────────────────────────
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Validate_Deterministic_SameSeed_SameResult()
|
|||
|
|
{
|
|||
|
|
const int N = 150;
|
|||
|
|
const int period = 13;
|
|||
|
|
|
|||
|
|
static (double bull, double bear) Run(int seed)
|
|||
|
|
{
|
|||
|
|
var gbm = new GBM(100.0, 0.05, 0.2, seed: seed);
|
|||
|
|
var eri = new Eri(period);
|
|||
|
|
for (int i = 0; i < N; i++)
|
|||
|
|
{
|
|||
|
|
eri.Update(gbm.Next(isNew: true), isNew: true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (eri.Last.Value, eri.BearPower);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var (bull1, bear1) = Run(5555);
|
|||
|
|
var (bull2, bear2) = Run(5555);
|
|||
|
|
|
|||
|
|
Assert.Equal(bull1, bull2, 1e-14);
|
|||
|
|
Assert.Equal(bear1, bear2, 1e-14);
|
|||
|
|
_output.WriteLine($"Deterministic BullPower={bull1:F10}, BearPower={bear1:F10}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── I) Sign symmetry: Bull + Bear = High + Low − 2×EMA ──────────────
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Validate_BullPlusBear_Equals_HighPlusLowMinusTwoEma()
|
|||
|
|
{
|
|||
|
|
// BullPower = High - EMA, BearPower = Low - EMA
|
|||
|
|
// Therefore BullPower + BearPower = High + Low - 2*EMA
|
|||
|
|
// We cannot directly observe EMA, but we CAN validate via Skender's Ema field.
|
|||
|
|
const int period = DefaultPeriod;
|
|||
|
|
int n = _data.Bars.Count;
|
|||
|
|
|
|||
|
|
var eri = new Eri(period);
|
|||
|
|
var bullSeries = new double[n];
|
|||
|
|
var bearSeries = new double[n];
|
|||
|
|
|
|||
|
|
for (int i = 0; i < n; i++)
|
|||
|
|
{
|
|||
|
|
bullSeries[i] = eri.Update(_data.Bars[i], isNew: true).Value;
|
|||
|
|
bearSeries[i] = eri.BearPower;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var sResults = _data.SkenderQuotes.GetElderRay(period).ToList();
|
|||
|
|
|
|||
|
|
int skip = ValidationHelper.DefaultVerificationCount;
|
|||
|
|
int count = Math.Min(n, sResults.Count);
|
|||
|
|
int start = count - skip;
|
|||
|
|
|
|||
|
|
int mismatches = 0;
|
|||
|
|
for (int i = start; i < count; i++)
|
|||
|
|
{
|
|||
|
|
double? sEma = sResults[i].Ema;
|
|||
|
|
if (!sEma.HasValue)
|
|||
|
|
{
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
double high = _data.HighPrices.Span[i];
|
|||
|
|
double low = _data.LowPrices.Span[i];
|
|||
|
|
double expected = high + low - (2.0 * sEma.Value);
|
|||
|
|
double actual = bullSeries[i] + bearSeries[i];
|
|||
|
|
|
|||
|
|
double diff = Math.Abs(actual - expected);
|
|||
|
|
if (diff > Tolerance)
|
|||
|
|
{
|
|||
|
|
mismatches++;
|
|||
|
|
_output.WriteLine($"Sum mismatch [i={i}]: actual={actual:F10}, expected={expected:F10}, diff={diff:E3}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Assert.Equal(0, mismatches);
|
|||
|
|
_output.WriteLine($"Bull+Bear = High+Low-2*EMA identity validated for last {skip} bars");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── J) Output finiteness after warmup ───────────────────────────────
|
|||
|
|
|
|||
|
|
[Fact]
|
|||
|
|
public void Validate_AllOutputsFinite_AfterWarmup()
|
|||
|
|
{
|
|||
|
|
const int period = DefaultPeriod;
|
|||
|
|
int n = _data.Bars.Count;
|
|||
|
|
int warmup = period;
|
|||
|
|
|
|||
|
|
var eri = new Eri(period);
|
|||
|
|
int nonFiniteCount = 0;
|
|||
|
|
|
|||
|
|
for (int i = 0; i < n; i++)
|
|||
|
|
{
|
|||
|
|
eri.Update(_data.Bars[i], isNew: true);
|
|||
|
|
|
|||
|
|
if (i >= warmup)
|
|||
|
|
{
|
|||
|
|
if (!double.IsFinite(eri.Last.Value))
|
|||
|
|
{
|
|||
|
|
nonFiniteCount++;
|
|||
|
|
_output.WriteLine($"Non-finite BullPower at i={i}: {eri.Last.Value}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!double.IsFinite(eri.BearPower))
|
|||
|
|
{
|
|||
|
|
nonFiniteCount++;
|
|||
|
|
_output.WriteLine($"Non-finite BearPower at i={i}: {eri.BearPower}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Assert.Equal(0, nonFiniteCount);
|
|||
|
|
_output.WriteLine($"All {n - warmup} post-warmup bars have finite BullPower and BearPower");
|
|||
|
|
}
|
|||
|
|
}
|