mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-26 06:18:05 +00:00
feat: add FSI (Ehlers Fourier Series Indicator, TASC June 2019)
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class FsiIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void FsiIndicator_BasicProperties()
|
||||
{
|
||||
var indicator = new FsiIndicator();
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(0.1, indicator.Bandwidth, 10);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FsiIndicator_Name_ContainsEhlers()
|
||||
{
|
||||
var indicator = new FsiIndicator();
|
||||
Assert.Contains("Ehlers", indicator.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FsiIndicator_Name_ContainsFSI()
|
||||
{
|
||||
var indicator = new FsiIndicator();
|
||||
Assert.Contains("FSI", indicator.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FsiIndicator_HasLineSeries()
|
||||
{
|
||||
var indicator = new FsiIndicator();
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FsiIndicator_SeparateWindow()
|
||||
{
|
||||
var indicator = new FsiIndicator();
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FsiIndicator_Initialize_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new FsiIndicator { Period = 30, Bandwidth = 0.2 };
|
||||
indicator.Initialize();
|
||||
indicator.HistoricalData.AddBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FsiIndicator_MultipleUpdates_ProducesFiniteValues()
|
||||
{
|
||||
var indicator = new FsiIndicator { Period = 20, Bandwidth = 0.1 };
|
||||
indicator.Initialize();
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double price = 100.0 + Math.Sin(2.0 * Math.PI * i / 20.0) * 5.0;
|
||||
indicator.HistoricalData.AddBar(
|
||||
DateTime.UtcNow.AddMinutes(i), price, price + 1, price - 1, price, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lastValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(lastValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FsiIndicator_BarCorrection_ProducesConsistentValues()
|
||||
{
|
||||
var indicator = new FsiIndicator { Period = 20, Bandwidth = 0.1 };
|
||||
indicator.Initialize();
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double price = 100.0 + i * 0.5;
|
||||
indicator.HistoricalData.AddBar(
|
||||
DateTime.UtcNow.AddMinutes(i), price, price + 1, price - 1, price, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// New bar
|
||||
indicator.HistoricalData.AddBar(
|
||||
DateTime.UtcNow.AddMinutes(30), 120, 121, 119, 120, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
double valAfterNew = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Correction (same bar, different price)
|
||||
indicator.HistoricalData.AddBar(
|
||||
DateTime.UtcNow.AddMinutes(30), 130, 131, 129, 130, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
double valAfterCorrection = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(valAfterNew));
|
||||
Assert.True(double.IsFinite(valAfterCorrection));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FsiIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new FsiIndicator { Period = 30, Bandwidth = 0.2 };
|
||||
Assert.Contains("30", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FsiIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new FsiIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Fsi.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FsiIndicator_DefaultSource_IsClose()
|
||||
{
|
||||
var indicator = new FsiIndicator();
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class FsiTests
|
||||
{
|
||||
private static readonly Random _rng = new(42);
|
||||
|
||||
private static TSeries MakeSeries(int count = 500)
|
||||
{
|
||||
var series = new TSeries();
|
||||
double price = 100.0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
price += (_rng.NextDouble() - 0.5) * 2.0;
|
||||
series.Add(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// A — Constructor
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters()
|
||||
{
|
||||
var fsi = new Fsi();
|
||||
Assert.Equal(20, fsi.Period);
|
||||
Assert.Equal(0.1, fsi.Bandwidth, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomParameters()
|
||||
{
|
||||
var fsi = new Fsi(period: 40, bandwidth: 0.2);
|
||||
Assert.Equal(40, fsi.Period);
|
||||
Assert.Equal(0.2, fsi.Bandwidth, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_PeriodTooSmall_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Fsi(period: 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_BandwidthTooSmall_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Fsi(period: 20, bandwidth: 0.0001));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// B — Basic Calculation
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void FirstBar_OutputIsZero()
|
||||
{
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
var result = fsi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SecondBar_OutputIsZero()
|
||||
{
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
fsi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
var result = fsi.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 101.0));
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThirdBar_OutputIsFinite()
|
||||
{
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
fsi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
fsi.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 101.0));
|
||||
var result = fsi.Update(new TValue(DateTime.UtcNow.AddMinutes(2), 102.0));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// C — State / Bar Correction
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void IsNew_True_AdvancesState()
|
||||
{
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
var series = MakeSeries(100);
|
||||
foreach (var bar in series)
|
||||
{
|
||||
fsi.Update(bar);
|
||||
}
|
||||
double val1 = fsi.Update(new TValue(DateTime.UtcNow, 105.0), isNew: true).Value;
|
||||
double val2 = fsi.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 110.0), isNew: true).Value;
|
||||
Assert.NotEqual(val1, val2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_False_CorrectionReproducible()
|
||||
{
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
var series = MakeSeries(100);
|
||||
foreach (var bar in series)
|
||||
{
|
||||
fsi.Update(bar);
|
||||
}
|
||||
|
||||
double v1 = fsi.Update(new TValue(DateTime.UtcNow, 105.0), isNew: true).Value;
|
||||
_ = fsi.Update(new TValue(DateTime.UtcNow, 108.0), isNew: false).Value;
|
||||
double v3 = fsi.Update(new TValue(DateTime.UtcNow, 105.0), isNew: false).Value;
|
||||
Assert.Equal(v1, v3, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
var series = MakeSeries(100);
|
||||
foreach (var bar in series)
|
||||
{
|
||||
fsi.Update(bar);
|
||||
}
|
||||
fsi.Reset();
|
||||
Assert.False(fsi.IsHot);
|
||||
Assert.Equal(0.0, fsi.Update(new TValue(DateTime.UtcNow, 100.0)).Value);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// D — Warmup
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FalseBeforePeriodBars()
|
||||
{
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
Assert.False(fsi.IsHot);
|
||||
fsi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.False(fsi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_TrueAfterPeriodBars()
|
||||
{
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
fsi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
Assert.True(fsi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_MatchesPeriod()
|
||||
{
|
||||
var fsi = new Fsi(30, 0.2);
|
||||
Assert.Equal(30, fsi.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// E — Robustness
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void LargeSeries_NoOverflow()
|
||||
{
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
var series = MakeSeries(5000);
|
||||
foreach (var bar in series)
|
||||
{
|
||||
fsi.Update(bar);
|
||||
}
|
||||
Assert.True(double.IsFinite(fsi.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VolatileInput_RemainsFinite()
|
||||
{
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
var rng = new Random(123);
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
double price = 100 + (rng.NextDouble() - 0.5) * 50;
|
||||
fsi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
Assert.True(double.IsFinite(fsi.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_Handled()
|
||||
{
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
fsi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
var result = fsi.Update(new TValue(DateTime.UtcNow.AddMinutes(30), double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// F — Consistency (4-API mode)
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResults()
|
||||
{
|
||||
var series = MakeSeries(300);
|
||||
int p = 20;
|
||||
double bw = 0.1;
|
||||
|
||||
// Mode 1: Streaming
|
||||
var streaming = new Fsi(p, bw);
|
||||
foreach (var bar in series)
|
||||
{
|
||||
streaming.Update(bar);
|
||||
}
|
||||
|
||||
// Mode 2: Batch TSeries
|
||||
var batchResult = Fsi.Batch(series, p, bw);
|
||||
|
||||
// Mode 3: Span
|
||||
var output = new double[series.Count];
|
||||
Fsi.Batch(series.Values, output, p, bw);
|
||||
|
||||
// Mode 4: Calculate
|
||||
var (calcResult, _) = Fsi.Calculate(series, p, bw);
|
||||
|
||||
// Compare last values
|
||||
double streamVal = streaming.Last.Value;
|
||||
double batchVal = batchResult[^1].Value;
|
||||
double spanVal = output[^1];
|
||||
double calcVal = calcResult[^1].Value;
|
||||
|
||||
Assert.Equal(streamVal, batchVal, 10);
|
||||
Assert.Equal(streamVal, spanVal, 10);
|
||||
Assert.Equal(streamVal, calcVal, 10);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// G — Span API
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesStreaming()
|
||||
{
|
||||
var series = MakeSeries(200);
|
||||
int p = 20;
|
||||
double bw = 0.1;
|
||||
|
||||
var streaming = new Fsi(p, bw);
|
||||
var streamResults = new double[series.Count];
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(series[i]).Value;
|
||||
}
|
||||
|
||||
var spanResults = new double[series.Count];
|
||||
Fsi.Batch(series.Values, spanResults, p, bw);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], spanResults[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_EmptyInput_NoThrow()
|
||||
{
|
||||
var exception = Record.Exception(() => Fsi.Batch(ReadOnlySpan<double>.Empty, Span<double>.Empty, 20, 0.1));
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MismatchedLengths_Throws()
|
||||
{
|
||||
var src = new double[10];
|
||||
var dst = new double[5];
|
||||
Assert.Throws<ArgumentException>(() => Fsi.Batch(src, dst, 20, 0.1));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// H — Chainability
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PubSub_ChainWorks()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var fsi = new Fsi(source, period: 20, bandwidth: 0.1);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i * 0.1));
|
||||
}
|
||||
Assert.True(double.IsFinite(fsi.Last.Value));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// FSI-Specific Behavioral Tests
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_OutputIsZero()
|
||||
{
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
fsi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||
}
|
||||
// Constant price → zero 2nd-order difference → BP = 0 → FSI = 0
|
||||
Assert.Equal(0.0, fsi.Last.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineWave_AtFundamental_ProducesOutput()
|
||||
{
|
||||
// Sine wave at period=20 (the fundamental) should produce significant output
|
||||
var fsi = new Fsi(20, 0.3);
|
||||
double lastAbsMax = 0;
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
fsi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
if (i > 100)
|
||||
{
|
||||
lastAbsMax = Math.Max(lastAbsMax, Math.Abs(fsi.Last.Value));
|
||||
}
|
||||
}
|
||||
Assert.True(lastAbsMax > 0.1,
|
||||
$"Expected significant output for fundamental sine, got max={lastAbsMax}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineWave_With2ndHarmonic_IncludesBoth()
|
||||
{
|
||||
// Composite sine with fundamental + 2nd harmonic
|
||||
var fsi = new Fsi(20, 0.3);
|
||||
double lastAbsMax = 0;
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
double price = 100.0 + 5.0 * Math.Sin(2.0 * Math.PI * i / 20.0)
|
||||
+ 3.0 * Math.Sin(2.0 * Math.PI * i / 10.0);
|
||||
fsi.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
if (i > 150)
|
||||
{
|
||||
lastAbsMax = Math.Max(lastAbsMax, Math.Abs(fsi.Last.Value));
|
||||
}
|
||||
}
|
||||
Assert.True(lastAbsMax > 0.1,
|
||||
$"Expected output for composite sine, got max={lastAbsMax}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var series = MakeSeries(300);
|
||||
var fsi1 = new Fsi(20, 0.1);
|
||||
var fsi2 = new Fsi(40, 0.1);
|
||||
foreach (var bar in series)
|
||||
{
|
||||
fsi1.Update(bar);
|
||||
fsi2.Update(bar);
|
||||
}
|
||||
Assert.NotEqual(fsi1.Last.Value, fsi2.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentBandwidths_ProduceDifferentResults()
|
||||
{
|
||||
var series = MakeSeries(300);
|
||||
var fsi1 = new Fsi(20, 0.1);
|
||||
var fsi2 = new Fsi(20, 0.5);
|
||||
foreach (var bar in series)
|
||||
{
|
||||
fsi1.Update(bar);
|
||||
fsi2.Update(bar);
|
||||
}
|
||||
Assert.NotEqual(fsi1.Last.Value, fsi2.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_IncludesPeriodAndBandwidth()
|
||||
{
|
||||
var fsi = new Fsi(30, 0.25);
|
||||
Assert.Contains("30", fsi.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("0.25", fsi.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsIndicatorAndResults()
|
||||
{
|
||||
var series = MakeSeries(200);
|
||||
var (results, indicator) = Fsi.Calculate(series, 20, 0.1);
|
||||
Assert.Equal(series.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_SetsState()
|
||||
{
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
var values = new double[100];
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
values[i] = 100.0 + i * 0.1;
|
||||
}
|
||||
fsi.Prime(values);
|
||||
Assert.True(fsi.IsHot);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(6)]
|
||||
[InlineData(10)]
|
||||
[InlineData(20)]
|
||||
[InlineData(50)]
|
||||
[InlineData(100)]
|
||||
public void VariousPeriods_AllFinite(int period)
|
||||
{
|
||||
var fsi = new Fsi(period, 0.1);
|
||||
var series = MakeSeries(500);
|
||||
foreach (var bar in series)
|
||||
{
|
||||
fsi.Update(bar);
|
||||
}
|
||||
Assert.True(double.IsFinite(fsi.Last.Value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class FsiValidationTests
|
||||
{
|
||||
private static readonly Random _rng = new(42);
|
||||
|
||||
private static TSeries MakeSeries(int count = 500)
|
||||
{
|
||||
var series = new TSeries();
|
||||
double price = 100.0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
price += (_rng.NextDouble() - 0.5) * 2.0;
|
||||
series.Add(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchStreaming_Match()
|
||||
{
|
||||
var series = MakeSeries(300);
|
||||
int period = 20;
|
||||
double bw = 0.1;
|
||||
|
||||
// Streaming
|
||||
var streaming = new Fsi(period, bw);
|
||||
var streamResults = new double[series.Count];
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(series[i]).Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Fsi.Batch(series, period, bw);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResult[i].Value, 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanStreaming_Match()
|
||||
{
|
||||
var series = MakeSeries(300);
|
||||
int period = 20;
|
||||
double bw = 0.1;
|
||||
|
||||
// Streaming
|
||||
var streaming = new Fsi(period, bw);
|
||||
var streamResults = new double[series.Count];
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(series[i]).Value;
|
||||
}
|
||||
|
||||
// Span batch
|
||||
var spanResults = new double[series.Count];
|
||||
Fsi.Batch(series.Values, spanResults, period, bw);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], spanResults[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceDifferentOutputs()
|
||||
{
|
||||
var series = MakeSeries(300);
|
||||
|
||||
var result1 = new double[series.Count];
|
||||
var result2 = new double[series.Count];
|
||||
Fsi.Batch(series.Values, result1, 20, 0.1);
|
||||
Fsi.Batch(series.Values, result2, 40, 0.1);
|
||||
|
||||
bool allEqual = true;
|
||||
for (int i = 50; i < series.Count; i++)
|
||||
{
|
||||
if (Math.Abs(result1[i] - result2[i]) > 1e-12)
|
||||
{
|
||||
allEqual = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert.False(allEqual, "Different periods should produce different outputs");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_ProducesZero()
|
||||
{
|
||||
int count = 200;
|
||||
var src = new double[count];
|
||||
var dst = new double[count];
|
||||
Array.Fill(src, 100.0);
|
||||
|
||||
Fsi.Batch(src, dst, 20, 0.1);
|
||||
|
||||
// After warmup, constant input → all-zero bandpass → output = 0
|
||||
for (int i = 20; i < count; i++)
|
||||
{
|
||||
Assert.Equal(0.0, dst[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
var series = MakeSeries(200);
|
||||
var (results, indicator) = Fsi.Calculate(series, 20, 0.1);
|
||||
Assert.Equal(series.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_Consistency()
|
||||
{
|
||||
var series = MakeSeries(100);
|
||||
var fsi = new Fsi(20, 0.1);
|
||||
|
||||
foreach (var bar in series)
|
||||
{
|
||||
fsi.Update(bar);
|
||||
}
|
||||
|
||||
// New bar
|
||||
double v1 = fsi.Update(new TValue(DateTime.UtcNow, 105.0), isNew: true).Value;
|
||||
|
||||
// Corrections
|
||||
_ = fsi.Update(new TValue(DateTime.UtcNow, 108.0), isNew: false);
|
||||
_ = fsi.Update(new TValue(DateTime.UtcNow, 112.0), isNew: false);
|
||||
double v4 = fsi.Update(new TValue(DateTime.UtcNow, 105.0), isNew: false).Value;
|
||||
|
||||
Assert.Equal(v1, v4, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubsetStability()
|
||||
{
|
||||
// Running on a longer series should not change earlier values
|
||||
var series = MakeSeries(300);
|
||||
int period = 20;
|
||||
double bw = 0.1;
|
||||
|
||||
var result200 = new double[200];
|
||||
Fsi.Batch(series.Values[..200], result200, period, bw);
|
||||
|
||||
var result300 = new double[300];
|
||||
Fsi.Batch(series.Values, result300, period, bw);
|
||||
|
||||
// First 200 bars of both runs must match exactly
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
Assert.Equal(result200[i], result300[i], 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user