mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-18 10:38:05 +00:00
feat: Add Prime method to various indicators for initializing state with historical data
- Implemented Prime method in Vel, Ao, Apo, Frama, Adl, Adosc, Aobv, Cmf, Efi, Eom, Iii, Kvo, Mfi, Nvi, Obv, Pvd, Pvi, Pvo, Pvr, Pvt, Tvi, Twap, Va, Vf, Vo, Vroc, Vwad, Vwap, and Vwma classes. - The Prime method resets the indicator state and processes the provided historical bar data to initialize the indicator. - Added warmup period property to Adl and Wad classes to define the minimum number of data points required for validity. - Updated benchmark tests to use Batch methods for performance evaluation.
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for HtDcperiodIndicator Quantower adapter.
|
||||
/// Covers: constructor, properties (Source, ShowColdValues, ShortName, MinHistoryDepths),
|
||||
/// OnInit, OnUpdate (HistoricalBar, NewBar, NewTick filtered), multiple bars,
|
||||
/// ShowColdValues false, reinitialize, source variants.
|
||||
/// </summary>
|
||||
public class HtDcperiodIndicatorTests
|
||||
{
|
||||
// ═══════════════════════════════ Constructor ═══════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InitializesName()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
Assert.Contains("HT_DCPERIOD", indicator.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InitializesDescription()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
Assert.False(string.IsNullOrEmpty(indicator.Description));
|
||||
Assert.Contains("Hilbert", indicator.Description, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SeparateWindowTrue()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_HasLineSeries()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
Assert.True(indicator.LinesSeries.Count >= 1);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════ Properties ═══════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Source_DefaultsToClose()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowColdValues_DefaultsToTrue()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortName_IsHtDcperiod()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
Assert.Equal("HT_DCPERIOD", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinHistoryDepths_Static_Is32()
|
||||
{
|
||||
Assert.Equal(32, HtDcperiodIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinHistoryDepths_Interface_Is32()
|
||||
{
|
||||
IWatchlistIndicator indicator = new HtDcperiodIndicator();
|
||||
Assert.Equal(32, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceCodeLink_IsNotEmpty()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
Assert.False(string.IsNullOrEmpty(indicator.SourceCodeLink));
|
||||
Assert.Contains("HtDcperiod", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════ OnInit ═══════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void OnInit_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.Initialize();
|
||||
// Should not throw - internal indicator created successfully
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════ OnUpdate ═════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void OnUpdate_HistoricalBar_Processes()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(
|
||||
time: now.AddMinutes(i),
|
||||
open: 100 + i,
|
||||
high: 105 + i,
|
||||
low: 95 + i,
|
||||
close: 102 + i,
|
||||
volume: 1000);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
Assert.Equal(40, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnUpdate_NewBar_Processes()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Feed historical bars first
|
||||
for (int i = 0; i < 35; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(
|
||||
time: now.AddMinutes(i),
|
||||
open: 100 + i,
|
||||
high: 105 + i,
|
||||
low: 95 + i,
|
||||
close: 102 + i,
|
||||
volume: 1000);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Then process a new bar
|
||||
indicator.HistoricalData.AddBar(
|
||||
time: now.AddMinutes(35),
|
||||
open: 135, high: 140, low: 130, close: 137, volume: 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(36, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnUpdate_NewTick_DoesNotThrow()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// NewTick should be filtered (early return) - no exception
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════ Multiple Bars ════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void OnUpdate_MultipleBars_ProducesFiniteValues()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// HT_DCPERIOD needs significant warmup - feed sinusoidal data
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100 + 10 * Math.Sin(i * 0.3);
|
||||
indicator.HistoricalData.AddBar(
|
||||
time: now.AddMinutes(i),
|
||||
open: price - 1,
|
||||
high: price + 2,
|
||||
low: price - 2,
|
||||
close: price,
|
||||
volume: 1000);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
Assert.Equal(100, indicator.LinesSeries[0].Count);
|
||||
|
||||
// After warmup, values should be finite
|
||||
double lastValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(lastValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnUpdate_SingleBar_ProducesValue()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════ ShowColdValues ═══════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ShowColdValues_CanBeSetFalse()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowColdValues_False_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.ShowColdValues = false;
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(
|
||||
time: now.AddMinutes(i),
|
||||
open: 100 + i, high: 105 + i, low: 95 + i,
|
||||
close: 102 + i, volume: 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════ Reinitialize ═════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Reinitialize_ResetsState()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(
|
||||
time: now.AddMinutes(i),
|
||||
open: 100 + i, high: 105 + i, low: 95 + i, close: 102 + i, volume: 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Reinitialize
|
||||
indicator.Initialize();
|
||||
|
||||
// Should process fresh data without error
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(
|
||||
time: now.AddMinutes(100 + i),
|
||||
open: 200 + i, high: 205 + i, low: 195 + i, close: 202 + i, volume: 2000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════ Source Variants ══════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Source_SetToOpen_Accepted()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.Source = SourceType.Open;
|
||||
Assert.Equal(SourceType.Open, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Source_SetToHigh_Accepted()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.Source = SourceType.High;
|
||||
Assert.Equal(SourceType.High, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Source_SetToLow_Accepted()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.Source = SourceType.Low;
|
||||
Assert.Equal(SourceType.Low, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Source_DifferentSources_ProcessWithoutError()
|
||||
{
|
||||
foreach (var source in new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close })
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.Source = source;
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(
|
||||
time: now.AddMinutes(i),
|
||||
open: 100 + i, high: 105 + i, low: 95 + i,
|
||||
close: 102 + i, volume: 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
}
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════ OnBackGround ═════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void OnBackGround_IsTrue()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════ Value Assertions ═════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Values_AfterWarmup_ArePositive()
|
||||
{
|
||||
var indicator = new HtDcperiodIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Feed sinusoidal data with known period (~21 bars)
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100 + 10 * Math.Sin(2 * Math.PI * i / 21.0);
|
||||
indicator.HistoricalData.AddBar(
|
||||
time: now.AddMinutes(i),
|
||||
open: price - 0.5,
|
||||
high: price + 1,
|
||||
low: price - 1,
|
||||
close: price,
|
||||
volume: 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Dominant cycle period should be positive after warmup
|
||||
double lastValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(lastValue > 0, $"Expected positive period, got {lastValue}");
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ namespace QuanTAlib.Tests.Cycles;
|
||||
|
||||
public class HtDcperiodTests
|
||||
{
|
||||
// ── Constructor ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsDefaults()
|
||||
{
|
||||
@@ -15,6 +17,38 @@ public class HtDcperiodTests
|
||||
Assert.False(ht.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithPublisher_SubscribesToEvents()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var ht = new HtDcperiod(source);
|
||||
Assert.False(ht.IsHot);
|
||||
|
||||
// Feed data through publisher
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + Math.Sin(i * 0.3) * 10));
|
||||
}
|
||||
|
||||
Assert.True(ht.IsHot);
|
||||
Assert.True(double.IsFinite(ht.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullPublisher_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new HtDcperiod(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Last_DefaultBeforeAnyUpdate()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
Assert.Equal(default, ht.Last);
|
||||
}
|
||||
|
||||
// ── IsHot & Warmup ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_BecomesHotAfterWarmup()
|
||||
{
|
||||
@@ -31,6 +65,171 @@ public class HtDcperiodTests
|
||||
Assert.True(double.IsFinite(ht.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FalseBeforeWarmup()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
ht.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
Assert.False(ht.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_Returns32()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
Assert.Equal(32, ht.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ── Update (streaming) ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBarsReturnZero()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
// During WMA initialization (first ~37 bars), output should be 0
|
||||
var result = ht.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ProducesFiniteValuesAfterWarmup()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
var gbm = new GBM(seed: 99);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
TValue lastResult = default;
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
lastResult = ht.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(lastResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PeriodInValidRange()
|
||||
{
|
||||
// The dominant cycle period should be clamped between 6 and 50
|
||||
var ht = new HtDcperiod();
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
bool anyHot = false;
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = ht.Update(new TValue(bar.Time, bar.Close));
|
||||
if (ht.IsHot)
|
||||
{
|
||||
anyHot = true;
|
||||
// Period output should be in a reasonable range
|
||||
Assert.True(double.IsFinite(result.Value),
|
||||
$"Period should be finite, got {result.Value}");
|
||||
}
|
||||
}
|
||||
Assert.True(anyHot);
|
||||
}
|
||||
|
||||
// ── Bar Correction (isNew=false) ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SameBarUpdate_ReturnsSameValue()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Prime with data
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ht.Update(new TValue(now.AddMinutes(i), 100 + Math.Sin(i * 0.1) * 10));
|
||||
}
|
||||
|
||||
Assert.True(ht.IsHot);
|
||||
|
||||
// First update (new bar)
|
||||
var result1 = ht.Update(new TValue(now.AddMinutes(50), 105), isNew: true);
|
||||
|
||||
// Same bar update with different price
|
||||
var result2 = ht.Update(new TValue(now.AddMinutes(50), 106), isNew: false);
|
||||
|
||||
// isNew=false should rollback and reapply - result should equal result1 since
|
||||
// bar correction restores previous state first
|
||||
Assert.Equal(result1.Value, result2.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_DoesNotAdvanceState()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ht.Update(new TValue(now.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
// New bar
|
||||
ht.Update(new TValue(now.AddMinutes(50), 150), isNew: true);
|
||||
var afterNew = ht.Last;
|
||||
|
||||
// Multiple corrections should not change the state relative to the new bar
|
||||
ht.Update(new TValue(now.AddMinutes(50), 151), isNew: false);
|
||||
ht.Update(new TValue(now.AddMinutes(50), 152), isNew: false);
|
||||
ht.Update(new TValue(now.AddMinutes(50), 150), isNew: false);
|
||||
var afterCorrections = ht.Last;
|
||||
|
||||
Assert.Equal(afterNew.Value, afterCorrections.Value);
|
||||
}
|
||||
|
||||
// ── NaN handling ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_BeforeAnyValidData_ReturnsZero()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
var result = ht.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValidPrice()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Feed valid data to warm up
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ht.Update(new TValue(now.AddMinutes(i), 100 + Math.Sin(i * 0.2) * 5));
|
||||
}
|
||||
|
||||
Assert.True(ht.IsHot);
|
||||
|
||||
// Feed NaN - should use last valid price
|
||||
var result = ht.Update(new TValue(now.AddMinutes(50), double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_UsesLastValidPrice()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ht.Update(new TValue(now.AddMinutes(i), 100 + i * 0.5));
|
||||
}
|
||||
|
||||
var result = ht.Update(new TValue(now.AddMinutes(50), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
// ── Reset ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
@@ -46,4 +245,237 @@ public class HtDcperiodTests
|
||||
Assert.False(ht.IsHot);
|
||||
Assert.Equal(default, ht.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReuse()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First use
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ht.Update(new TValue(now.AddMinutes(i), 100 + Math.Sin(i * 0.2) * 5));
|
||||
}
|
||||
Assert.True(ht.IsHot);
|
||||
var firstResult = ht.Last.Value;
|
||||
|
||||
// Reset and reuse
|
||||
ht.Reset();
|
||||
Assert.False(ht.IsHot);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ht.Update(new TValue(now.AddMinutes(i), 100 + Math.Sin(i * 0.2) * 5));
|
||||
}
|
||||
Assert.True(ht.IsHot);
|
||||
Assert.Equal(firstResult, ht.Last.Value);
|
||||
}
|
||||
|
||||
// ── Batch/TSeries Update ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_ReturnsCorrectCount()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var result = ht.Update(bars.Close);
|
||||
Assert.Equal(100, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_EmptyTSeries_ReturnsEmpty()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
var result = ht.Update(new TSeries());
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_TSeries_MatchesStreaming()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// Batch
|
||||
var batchResult = HtDcperiod.Batch(series);
|
||||
|
||||
// Streaming
|
||||
var streaming = new HtDcperiod();
|
||||
var streamingResults = new TSeries();
|
||||
foreach (var item in series)
|
||||
{
|
||||
streamingResults.Add(streaming.Update(item));
|
||||
}
|
||||
|
||||
// Compare
|
||||
Assert.Equal(batchResult.Count, streamingResults.Count);
|
||||
for (int i = 0; i < batchResult.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, streamingResults[i].Value, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_MatchesStreaming()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var values = bars.Close.Values.ToArray();
|
||||
|
||||
// Span batch
|
||||
var spanOutput = new double[values.Length];
|
||||
HtDcperiod.Batch(values, spanOutput);
|
||||
|
||||
// Streaming
|
||||
var streaming = new HtDcperiod();
|
||||
var streamingResults = new double[values.Length];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
streamingResults[i] = streaming.Update(new TValue(DateTime.UtcNow.AddTicks(i), values[i])).Value;
|
||||
}
|
||||
|
||||
// Compare
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], spanOutput[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_OutputTooShort_Throws()
|
||||
{
|
||||
var source = new double[10];
|
||||
var output = new double[5];
|
||||
Assert.Throws<ArgumentException>(() => HtDcperiod.Batch(source, output));
|
||||
}
|
||||
|
||||
// ── Calculate ───────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsBothResultsAndIndicator()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var (results, indicator) = HtDcperiod.Calculate(bars.Close);
|
||||
Assert.Equal(100, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
// ── Prime ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Prime_WamsUpIndicator()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(80, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var values = bars.Close.Values.ToArray();
|
||||
|
||||
ht.Prime(values);
|
||||
Assert.True(ht.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_WithStepParameter()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
var values = new double[50];
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
values[i] = 100 + Math.Sin(i * 0.2) * 5;
|
||||
}
|
||||
|
||||
ht.Prime(values, TimeSpan.FromMinutes(5));
|
||||
Assert.True(ht.IsHot);
|
||||
}
|
||||
|
||||
// ── Determinism ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TwoInstances_SameInput_SameOutput()
|
||||
{
|
||||
var ht1 = new HtDcperiod();
|
||||
var ht2 = new HtDcperiod();
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var tv = new TValue(bar.Time, bar.Close);
|
||||
var r1 = ht1.Update(tv);
|
||||
var r2 = ht2.Update(tv);
|
||||
Assert.Equal(r1.Value, r2.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Constant price ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ConstantPrice_ProducesFiniteOutput()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 80; i++)
|
||||
{
|
||||
var result = ht.Update(new TValue(now.AddMinutes(i), 100.0));
|
||||
Assert.True(double.IsFinite(result.Value),
|
||||
$"Bar {i}: Expected finite, got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sinusoidal input ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SinusoidalInput_DetectsApproximatePeriod()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Feed a clean sinusoidal with period ~20 bars
|
||||
int inputPeriod = 20;
|
||||
double omega = 2.0 * Math.PI / inputPeriod;
|
||||
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
ht.Update(new TValue(now.AddMinutes(i), 100 + 10 * Math.Sin(omega * i)));
|
||||
}
|
||||
|
||||
// After sufficient data, the detected period should be
|
||||
// somewhere in the ballpark of the input period
|
||||
Assert.True(ht.IsHot);
|
||||
double detected = ht.Last.Value;
|
||||
Assert.True(double.IsFinite(detected));
|
||||
// The Hilbert transform period detection is approximate
|
||||
Assert.True(detected >= 6.0 && detected <= 50.0,
|
||||
$"Detected period {detected} should be in [6, 50] range");
|
||||
}
|
||||
|
||||
// ── Dispose ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Dispose_DoesNotThrow()
|
||||
{
|
||||
var ht = new HtDcperiod();
|
||||
ht.Update(new TValue(DateTime.UtcNow, 100));
|
||||
ht.Dispose();
|
||||
Assert.True(true); // S2699: explicit assertion for dispose-only test
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WithPublisher_DoesNotThrow()
|
||||
{
|
||||
// HtDcperiod subscribes to source but does not track the source
|
||||
// reference for unsubscription — Dispose still must not throw
|
||||
var source = new TSeries();
|
||||
var ht = new HtDcperiod(source);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(double.IsFinite(ht.Last.Value) || ht.Last.Value == 0.0);
|
||||
ht.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user