mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 04:28:04 +00:00
docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class SqueezeIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void SqueezeIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new SqueezeIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(2.0, indicator.BbMult);
|
||||
Assert.Equal(1.5, indicator.KcMult);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("SQUEEZE", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new SqueezeIndicator { Period = 20, BbMult = 2.0, KcMult = 1.5 };
|
||||
|
||||
Assert.Equal(0, SqueezeIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new SqueezeIndicator { Period = 20, BbMult = 2.0, KcMult = 1.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("SQUEEZE", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new SqueezeIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Squeeze", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_Initialize_CreatesTwoLineSeries()
|
||||
{
|
||||
var indicator = new SqueezeIndicator { Period = 20, BbMult = 2.0, KcMult = 1.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Momentum + SqueezeOn
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SqueezeIndicator { Period = 5, BbMult = 2.0, KcMult = 1.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 100.0 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 1, price - 1, price);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double mom = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(mom));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SqueezeIndicator { Period = 5, BbMult = 2.0, KcMult = 1.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double price = 100.0 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 1, price - 1, price);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Simulate a new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 112, 108, 111);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double mom = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(mom));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_DifferentOhlcSources_Supported()
|
||||
{
|
||||
var indicator = new SqueezeIndicator { Period = 5, BbMult = 2.0, KcMult = 1.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
double price = 50.0 + i * 0.5;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 0.5, price - 0.5, price + 0.1);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class SqueezeTests
|
||||
{
|
||||
private static TBarSeries GenerateBars(int count, int seed = 42)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: seed);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
// === A) Constructor validation ===
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidPeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Squeeze(period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Squeeze(period: -1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidBbMult_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Squeeze(period: 20, bbMult: 0.0));
|
||||
Assert.Equal("bbMult", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeBbMult_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Squeeze(period: 20, bbMult: -1.0));
|
||||
Assert.Equal("bbMult", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidKcMult_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Squeeze(period: 20, kcMult: 0.0));
|
||||
Assert.Equal("kcMult", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeKcMult_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Squeeze(period: 20, kcMult: -1.5));
|
||||
Assert.Equal("kcMult", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParams()
|
||||
{
|
||||
var sq = new Squeeze();
|
||||
Assert.Equal("Squeeze(20,2,1.5)", sq.Name);
|
||||
Assert.Equal(20, sq.WarmupPeriod);
|
||||
}
|
||||
|
||||
// === B) Basic calculation ===
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 101, 1000);
|
||||
TValue result = sq.Update(bar);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Last_Momentum_Accessible()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100 + i, 105 + i, 95 + i, 101 + i, 1000);
|
||||
sq.Update(bar);
|
||||
}
|
||||
Assert.True(double.IsFinite(sq.Last.Value));
|
||||
Assert.True(double.IsFinite(sq.Momentum));
|
||||
Assert.NotEmpty(sq.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SqueezeOn_IsBool()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 101, 99, 100, 1000);
|
||||
sq.Update(bar);
|
||||
}
|
||||
// SqueezeOn is a bool — either value is valid; assert it's a well-formed bool (not default struct garbage)
|
||||
Assert.True(sq.SqueezeOn || !sq.SqueezeOn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantBars_MomentumNearZero()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
sq.Update(bar);
|
||||
}
|
||||
// With constant price, delta = 0 for all bars, so momentum = 0
|
||||
Assert.Equal(0.0, sq.Momentum, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RisingBars_PositiveMomentum_AfterWarmup()
|
||||
{
|
||||
var sq = new Squeeze(period: 10);
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
double price = 100.0 + i;
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), price, price + 1, price - 1, price, 1000);
|
||||
sq.Update(bar);
|
||||
}
|
||||
Assert.True(sq.IsHot);
|
||||
Assert.True(sq.Momentum > 0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FallingBars_NegativeMomentum_AfterWarmup()
|
||||
{
|
||||
var sq = new Squeeze(period: 10);
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
double price = 200.0 - i;
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), price, price + 1, price - 1, price, 1000);
|
||||
sq.Update(bar);
|
||||
}
|
||||
Assert.True(sq.IsHot);
|
||||
Assert.True(sq.Momentum < 0.0);
|
||||
}
|
||||
|
||||
// === C) State + bar correction ===
|
||||
|
||||
[Fact]
|
||||
public void IsNew_True_Advances_State()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bars = GenerateBars(10);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
double momBefore = sq.Momentum;
|
||||
|
||||
var nextBar = new TBar(DateTime.UtcNow.AddMinutes(100), 200, 210, 190, 205, 1000);
|
||||
sq.Update(nextBar, isNew: true);
|
||||
|
||||
// New bar with very different price should move momentum
|
||||
Assert.True(double.IsFinite(sq.Momentum));
|
||||
_ = momBefore; // silence unused var
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_False_Rewrites()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bars = GenerateBars(10);
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
sq.Update(bars[9], isNew: true);
|
||||
double momAfterNew = sq.Momentum;
|
||||
|
||||
// Rewrite bar 9 with very different OHLC
|
||||
var corrected = new TBar(bars[9].Time, 999, 1005, 990, 1000, 1000);
|
||||
sq.Update(corrected, isNew: false);
|
||||
double momAfterCorrect = sq.Momentum;
|
||||
|
||||
Assert.NotEqual(momAfterNew, momAfterCorrect);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrection_Restores()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bars = GenerateBars(15);
|
||||
for (int i = 0; i < 14; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Feed bar 14 for real
|
||||
sq.Update(bars[14], isNew: true);
|
||||
double momAfterTrue = sq.Momentum;
|
||||
|
||||
// Simulate 3 re-updates of same bar
|
||||
for (int j = 0; j < 3; j++)
|
||||
{
|
||||
sq.Update(bars[14], isNew: false);
|
||||
}
|
||||
|
||||
// Correction with same value should restore same momentum
|
||||
Assert.Equal(momAfterTrue, sq.Momentum, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bars = GenerateBars(20);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
sq.Reset();
|
||||
|
||||
Assert.False(sq.IsHot);
|
||||
Assert.Equal(0.0, sq.Momentum);
|
||||
Assert.False(sq.SqueezeOn);
|
||||
}
|
||||
|
||||
// === D) Warmup/convergence ===
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsAtPeriod()
|
||||
{
|
||||
var sq = new Squeeze(period: 10);
|
||||
var bars = GenerateBars(20);
|
||||
bool hotBefore = false;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
hotBefore = sq.IsHot;
|
||||
}
|
||||
Assert.True(sq.IsHot);
|
||||
_ = hotBefore;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_MatchesPeriod()
|
||||
{
|
||||
var sq = new Squeeze(period: 15);
|
||||
Assert.Equal(15, sq.WarmupPeriod);
|
||||
}
|
||||
|
||||
// === E) Robustness ===
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValid()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bars = GenerateBars(10);
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
double momBefore = sq.Momentum;
|
||||
|
||||
var nanBar = new TBar(DateTime.UtcNow.AddMinutes(100), double.NaN, double.NaN, double.NaN, double.NaN, 0);
|
||||
sq.Update(nanBar, isNew: true);
|
||||
|
||||
// Should return NaN or substitute last-valid — either way must not throw
|
||||
Assert.True(true);
|
||||
_ = momBefore;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_Handled()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bars = GenerateBars(10);
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
var infBar = new TBar(DateTime.UtcNow.AddMinutes(100),
|
||||
double.PositiveInfinity, double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity, 0);
|
||||
// must not throw
|
||||
sq.Update(infBar, isNew: true);
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_ResultFiniteOrNaN()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
TBar bar;
|
||||
if (i % 5 == 0)
|
||||
{
|
||||
bar = new TBar(DateTime.UtcNow.AddMinutes(i), double.NaN, double.NaN, double.NaN, double.NaN, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100 + i, 105 + i, 95 + i, 101 + i, 1000);
|
||||
}
|
||||
sq.Update(bar, isNew: true);
|
||||
}
|
||||
// Must complete without exception
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
// === F) Consistency ===
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
const int period = 10;
|
||||
|
||||
// Streaming
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
double streamMom = sq.Momentum;
|
||||
|
||||
// Batch static
|
||||
var (batchMom, _) = Squeeze.Batch(bars, period);
|
||||
double batchLast = batchMom[^1].Value;
|
||||
|
||||
Assert.Equal(streamMom, batchLast, precision: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
const int period = 10;
|
||||
|
||||
// Streaming
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
double streamMom = sq.Momentum;
|
||||
|
||||
// Span Batch
|
||||
double[] momOut = new double[50];
|
||||
double[] sqOut = new double[50];
|
||||
Squeeze.Batch(bars.HighValues, bars.LowValues, bars.CloseValues,
|
||||
momOut, sqOut, period);
|
||||
double spanLast = momOut[49];
|
||||
|
||||
Assert.Equal(streamMom, spanLast, precision: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventingMode_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
const int period = 10;
|
||||
|
||||
// Streaming
|
||||
var sqStream = new Squeeze(period);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
sqStream.Update(bars[i], isNew: true);
|
||||
}
|
||||
double streamMom = sqStream.Momentum;
|
||||
|
||||
// Eventing via TBarSeries constructor
|
||||
var sqEvent = new Squeeze(bars, period);
|
||||
Assert.Equal(streamMom, sqEvent.Momentum, precision: 6);
|
||||
}
|
||||
|
||||
// === G) Span API tests ===
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_ThrowsOnInvalidPeriod()
|
||||
{
|
||||
double[] h = [100, 101, 102];
|
||||
double[] l = [99, 100, 101];
|
||||
double[] c = [100, 101, 102];
|
||||
double[] mom = new double[3];
|
||||
double[] sq = new double[3];
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Squeeze.Batch(h, l, c, mom, sq, period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_ThrowsOnMismatchedLengths()
|
||||
{
|
||||
double[] h = [100, 101];
|
||||
double[] l = [99];
|
||||
double[] c = [100, 101];
|
||||
double[] mom = new double[2];
|
||||
double[] sq = new double[2];
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Squeeze.Batch(h, l, c, mom, sq, period: 5));
|
||||
Assert.Equal("high", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_ThrowsOnShortMomOutput()
|
||||
{
|
||||
double[] h = [100, 101, 102, 103, 104];
|
||||
double[] l = [99, 100, 101, 102, 103];
|
||||
double[] c = [100, 101, 102, 103, 104];
|
||||
double[] mom = new double[2]; // too short
|
||||
double[] sq = new double[5];
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Squeeze.Batch(h, l, c, mom, sq, period: 3));
|
||||
Assert.Equal("momOut", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_ThrowsOnShortSqOutput()
|
||||
{
|
||||
double[] h = [100, 101, 102, 103, 104];
|
||||
double[] l = [99, 100, 101, 102, 103];
|
||||
double[] c = [100, 101, 102, 103, 104];
|
||||
double[] mom = new double[5];
|
||||
double[] sq = new double[2]; // too short
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Squeeze.Batch(h, l, c, mom, sq, period: 3));
|
||||
Assert.Equal("sqOut", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_LargeData_NoStackOverflow()
|
||||
{
|
||||
const int size = 2000;
|
||||
var gbm = new GBM(100.0, 0.02, 0.15, seed: 1);
|
||||
var bars = gbm.Fetch(size, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
double[] mom = new double[size];
|
||||
double[] sq = new double[size];
|
||||
// period = 300 forces ArrayPool path
|
||||
Squeeze.Batch(bars.HighValues, bars.LowValues, bars.CloseValues, mom, sq, period: 300);
|
||||
Assert.True(double.IsFinite(mom[size - 1]));
|
||||
}
|
||||
|
||||
// === H) Chainability ===
|
||||
|
||||
[Fact]
|
||||
public void PubEvent_Fires()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
int fireCount = 0;
|
||||
sq.Pub += (_, in e) => fireCount++;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 105, 95, 101, 1000);
|
||||
sq.Update(bar, isNew: true);
|
||||
}
|
||||
Assert.Equal(10, fireCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBarSeries_Constructor_Subscribes()
|
||||
{
|
||||
var bars = GenerateBars(30);
|
||||
var sq = new Squeeze(bars, period: 10);
|
||||
|
||||
Assert.True(sq.IsHot);
|
||||
Assert.True(double.IsFinite(sq.Momentum));
|
||||
}
|
||||
|
||||
// === Calculate static factory ===
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var bars = GenerateBars(30);
|
||||
var ((momSeries, sqSeries), indicator) = Squeeze.Calculate(bars, period: 10);
|
||||
|
||||
Assert.Equal(30, momSeries.Count);
|
||||
Assert.Equal(30, sqSeries.Count);
|
||||
Assert.NotNull(indicator);
|
||||
Assert.True(double.IsFinite(indicator.Momentum));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for Squeeze — internal consistency checks.
|
||||
/// No external library implements this indicator identically, so we validate
|
||||
/// against known mathematical properties and self-consistency.
|
||||
/// </summary>
|
||||
public sealed class SqueezeValidationTests
|
||||
{
|
||||
private static TBarSeries GenerateBars(int count, int seed = 42)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: seed);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
// 1. Streaming == Batch (TBarSeries) consistency
|
||||
[Fact]
|
||||
public void Streaming_MatchesBatch_Momentum()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
const int period = 20;
|
||||
|
||||
// Streaming
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
double streamMom = sq.Momentum;
|
||||
|
||||
// Batch
|
||||
var (momSeries, _) = Squeeze.Batch(bars, period);
|
||||
double batchMom = momSeries[^1].Value;
|
||||
|
||||
Assert.Equal(streamMom, batchMom, precision: 8);
|
||||
}
|
||||
|
||||
// 2. Streaming == Batch (TBarSeries) for SqueezeOn state
|
||||
[Fact]
|
||||
public void Streaming_MatchesBatch_SqueezeOn()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
const int period = 20;
|
||||
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
bool streamSqOn = sq.SqueezeOn;
|
||||
|
||||
var (_, sqSeries) = Squeeze.Batch(bars, period);
|
||||
bool batchSqOn = sqSeries[^1].Value >= 0.5;
|
||||
|
||||
Assert.Equal(streamSqOn, batchSqOn);
|
||||
}
|
||||
|
||||
// 3. Span Batch == Streaming
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
const int period = 20;
|
||||
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
double streamMom = sq.Momentum;
|
||||
|
||||
double[] momOut = new double[100];
|
||||
double[] sqOut = new double[100];
|
||||
Squeeze.Batch(bars.HighValues, bars.LowValues, bars.CloseValues,
|
||||
momOut, sqOut, period);
|
||||
|
||||
Assert.Equal(streamMom, momOut[99], precision: 8);
|
||||
}
|
||||
|
||||
// 4. Constant price → zero momentum (delta always 0)
|
||||
[Fact]
|
||||
public void ConstantPrice_ZeroMomentum()
|
||||
{
|
||||
const int period = 10;
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
sq.Update(bar, isNew: true);
|
||||
}
|
||||
Assert.Equal(0.0, sq.Momentum, precision: 10);
|
||||
}
|
||||
|
||||
// 5. Rising price → positive momentum (linreg endpoint positive)
|
||||
[Fact]
|
||||
public void RisingPrice_PositiveMomentum()
|
||||
{
|
||||
const int period = 10;
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double p = 100.0 + i;
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), p, p + 1, p - 1, p, 1000);
|
||||
sq.Update(bar, isNew: true);
|
||||
}
|
||||
Assert.True(sq.Momentum > 0.0);
|
||||
}
|
||||
|
||||
// 6. Falling price → negative momentum
|
||||
[Fact]
|
||||
public void FallingPrice_NegativeMomentum()
|
||||
{
|
||||
const int period = 10;
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double p = 200.0 - i;
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), p, p + 1, p - 1, p, 1000);
|
||||
sq.Update(bar, isNew: true);
|
||||
}
|
||||
Assert.True(sq.Momentum < 0.0);
|
||||
}
|
||||
|
||||
// 7. Very tight range → BB inside KC → squeeze should be ON
|
||||
[Fact]
|
||||
public void VeryTightRange_SqueezeOn_True()
|
||||
{
|
||||
// Extremely tight range → stddev very small → BB narrows inside KC
|
||||
const int period = 20;
|
||||
var sq = new Squeeze(period, bbMult: 2.0, kcMult: 1.5);
|
||||
// Use tiny sigma so BB << KC
|
||||
var gbm = new GBM(100.0, 0.0, 0.001, seed: 99); // near-constant with tiny noise
|
||||
var bars = gbm.Fetch(60, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
// After 60 bars with near-zero sigma, BB should be inside KC
|
||||
Assert.True(sq.SqueezeOn);
|
||||
}
|
||||
|
||||
// 8. Very high volatility → BB outside KC → squeeze should be OFF
|
||||
[Fact]
|
||||
public void HighVolatility_SqueezeOn_False()
|
||||
{
|
||||
const int period = 20;
|
||||
var sq = new Squeeze(period, bbMult: 2.0, kcMult: 1.5);
|
||||
// Use very high sigma so BB >> KC
|
||||
var gbm = new GBM(100.0, 0.0, 5.0, seed: 77); // wild swings
|
||||
var bars = gbm.Fetch(60, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
Assert.False(sq.SqueezeOn);
|
||||
}
|
||||
|
||||
// 9. Period=1 edge case — should not crash
|
||||
[Fact]
|
||||
public void Period1_DoesNotCrash()
|
||||
{
|
||||
var sq = new Squeeze(period: 1);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double p = 100.0 + i;
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), p, p + 1, p - 1, p, 1000);
|
||||
sq.Update(bar, isNew: true);
|
||||
}
|
||||
Assert.True(double.IsFinite(sq.Momentum));
|
||||
}
|
||||
|
||||
// 10. Bar correction: feeding same bar multiple times with isNew=false restores original result
|
||||
[Fact]
|
||||
public void MultipleCorrections_Idempotent()
|
||||
{
|
||||
var bars = GenerateBars(25);
|
||||
const int period = 10;
|
||||
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < 24; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
sq.Update(bars[24], isNew: true);
|
||||
double momRef = sq.Momentum;
|
||||
|
||||
// Correct 3 more times with same bar
|
||||
for (int k = 0; k < 3; k++)
|
||||
{
|
||||
sq.Update(bars[24], isNew: false);
|
||||
}
|
||||
Assert.Equal(momRef, sq.Momentum, precision: 10);
|
||||
}
|
||||
|
||||
// 11. Update(TBarSeries) === streaming
|
||||
[Fact]
|
||||
public void UpdateTBarSeries_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
const int period = 10;
|
||||
|
||||
// Streaming
|
||||
var sqStream = new Squeeze(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
sqStream.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// TBarSeries update
|
||||
var sqBatch = new Squeeze(period);
|
||||
_ = sqBatch.Update(bars);
|
||||
|
||||
Assert.Equal(sqStream.Momentum, sqBatch.Momentum, precision: 8);
|
||||
Assert.Equal(sqStream.SqueezeOn, sqBatch.SqueezeOn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Squeeze_Correction_Recomputes()
|
||||
{
|
||||
var ind = new Squeeze(period: 20);
|
||||
long t0 = TimeSpan.TicksPerSecond;
|
||||
|
||||
// Build state well past warmup (WarmupPeriod = 20)
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double p = 100.0 + (i * 0.5);
|
||||
ind.Update(new TBar(t0 + (i * TimeSpan.TicksPerSecond), p, p + 1, p - 1, p, 1000), isNew: true);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
long anchorTime = t0 + (50 * TimeSpan.TicksPerSecond);
|
||||
var anchorBar = new TBar(anchorTime, 125.0, 126.0, 124.0, 125.0, 1000);
|
||||
ind.Update(anchorBar, isNew: true);
|
||||
double anchorMomentum = ind.Momentum;
|
||||
bool anchorSqueezeOn = ind.SqueezeOn;
|
||||
|
||||
// Correction with dramatically different values — Momentum must change
|
||||
var corruptBar = new TBar(anchorTime, 1250.0, 1260.0, 1240.0, 1250.0, 1000);
|
||||
ind.Update(corruptBar, isNew: false);
|
||||
Assert.NotEqual(anchorMomentum, ind.Momentum);
|
||||
|
||||
// Correction back to original — both outputs must restore exactly
|
||||
ind.Update(anchorBar, isNew: false);
|
||||
Assert.Equal(anchorMomentum, ind.Momentum, 1e-9);
|
||||
Assert.Equal(anchorSqueezeOn, ind.SqueezeOn);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user