mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 12:38:06 +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,114 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TtmSqueezeIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void TtmSqueezeIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new TtmSqueezeIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.BbPeriod);
|
||||
Assert.Equal(2.0, indicator.BbMult);
|
||||
Assert.Equal(20, indicator.KcPeriod);
|
||||
Assert.Equal(1.5, indicator.KcMult);
|
||||
Assert.Equal(20, indicator.MomPeriod);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("TTM Squeeze", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TtmSqueezeIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new TtmSqueezeIndicator { BbPeriod = 20 };
|
||||
|
||||
Assert.Equal(0, TtmSqueezeIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TtmSqueezeIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new TtmSqueezeIndicator
|
||||
{
|
||||
BbPeriod = 15,
|
||||
BbMult = 1.5,
|
||||
KcPeriod = 10,
|
||||
KcMult = 2.0,
|
||||
MomPeriod = 25
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("TTM_SQZ", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("10", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("25", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TtmSqueezeIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new TtmSqueezeIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("TtmSqueeze.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TtmSqueezeIndicator_Initialize_CreatesInternalSqueeze()
|
||||
{
|
||||
var indicator = new TtmSqueezeIndicator
|
||||
{
|
||||
BbPeriod = 14,
|
||||
KcPeriod = 14,
|
||||
MomPeriod = 14
|
||||
};
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (momentum + squeeze)
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TtmSqueezeIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TtmSqueezeIndicator
|
||||
{
|
||||
BbPeriod = 5,
|
||||
KcPeriod = 5,
|
||||
MomPeriod = 5
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double momentum = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(momentum));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TtmSqueezeIndicator_TwoLineSeries_Exist()
|
||||
{
|
||||
var indicator = new TtmSqueezeIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// Should have momentum + squeeze dot series
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TtmSqueezeTests
|
||||
{
|
||||
private const double Precision = 1e-10;
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_AreCorrect()
|
||||
{
|
||||
var squeeze = new TtmSqueeze();
|
||||
Assert.Equal(20, squeeze.BbPeriod);
|
||||
Assert.Equal(20, squeeze.KcPeriod);
|
||||
Assert.Equal(20, squeeze.MomPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomParameters_AreSet()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 15, bbMult: 1.5, kcPeriod: 10, kcMult: 2.0, momPeriod: 25);
|
||||
Assert.Equal(15, squeeze.BbPeriod);
|
||||
Assert.Equal(10, squeeze.KcPeriod);
|
||||
Assert.Equal(25, squeeze.MomPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidBbPeriod_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new TtmSqueeze(bbPeriod: 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidKcPeriod_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new TtmSqueeze(kcPeriod: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidMomPeriod_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new TtmSqueeze(momPeriod: 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidBbMult_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new TtmSqueeze(bbMult: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidKcMult_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new TtmSqueeze(kcMult: -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_IncludesAllParameters()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(15, 1.5, 10, 2.0, 25);
|
||||
Assert.Contains("15", squeeze.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("1.5", squeeze.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("10", squeeze.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("2.0", squeeze.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("25", squeeze.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsMaxOfPeriods()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 15, bbMult: 2.0, kcPeriod: 10, kcMult: 1.5, momPeriod: 25);
|
||||
Assert.Equal(25, squeeze.WarmupPeriod);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsHot Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BeforeWarmup_ReturnsFalse()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 5, bbMult: 2.0, kcPeriod: 5, kcMult: 1.5, momPeriod: 5);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + i * 60000, 100, 105, 95, 102, 1000));
|
||||
}
|
||||
|
||||
Assert.False(squeeze.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterWarmup_ReturnsTrue()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 5, bbMult: 2.0, kcPeriod: 5, kcMult: 1.5, momPeriod: 5);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + i * 60000, 100, 105, 95, 102, 1000));
|
||||
}
|
||||
|
||||
Assert.True(squeeze.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Squeeze Detection Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_LowVolatility_SqueezeOn()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 5, bbMult: 2.0, kcPeriod: 5, kcMult: 1.5, momPeriod: 5);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Low volatility: tight range bars
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + i * 60000, 100, 100.5, 99.5, 100, 1000));
|
||||
}
|
||||
|
||||
// With tight range (0.5 from mid), low stddev means BB should be tighter
|
||||
// This should trigger squeeze on
|
||||
// Note: May need specific values depending on implementation
|
||||
Assert.True(double.IsFinite(squeeze.Momentum.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_HighVolatility_SqueezeOff()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 5, bbMult: 2.0, kcPeriod: 5, kcMult: 1.5, momPeriod: 5);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// High volatility: wide range bars
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double offset = (i % 2 == 0) ? 10 : -10;
|
||||
squeeze.Update(new TBar(baseTime + i * 60000, 100, 110 + offset, 90 + offset, 100 + offset, 1000));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(squeeze.Momentum.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SqueezeFired_DetectedOnTransition()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Start with tight range (likely squeeze on)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + i * 60000, 100, 100.1, 99.9, 100, 1000));
|
||||
}
|
||||
|
||||
// Sudden volatility expansion (removed unused initialSqueezeOn variable)
|
||||
squeeze.Update(new TBar(baseTime + 5 * 60000, 100, 120, 80, 115, 1000));
|
||||
|
||||
// The squeeze state should have changed
|
||||
// (The exact behavior depends on the calculation)
|
||||
Assert.True(double.IsFinite(squeeze.Momentum.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Momentum Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_PriceAboveMidline_PositiveMomentum()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Prices consistently above the donchian midline
|
||||
squeeze.Update(new TBar(baseTime, 100, 102, 98, 101, 1000));
|
||||
squeeze.Update(new TBar(baseTime + 60000, 101, 103, 99, 102, 1000));
|
||||
squeeze.Update(new TBar(baseTime + 120000, 102, 104, 100, 103, 1000));
|
||||
squeeze.Update(new TBar(baseTime + 180000, 103, 106, 101, 105, 1000));
|
||||
|
||||
// With rising prices, momentum should be positive
|
||||
Assert.True(squeeze.MomentumPositive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PriceBelowMidline_NegativeMomentum()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Prices consistently below the donchian midline
|
||||
squeeze.Update(new TBar(baseTime, 100, 102, 98, 99, 1000));
|
||||
squeeze.Update(new TBar(baseTime + 60000, 99, 101, 97, 98, 1000));
|
||||
squeeze.Update(new TBar(baseTime + 120000, 98, 100, 96, 97, 1000));
|
||||
squeeze.Update(new TBar(baseTime + 180000, 97, 99, 95, 96, 1000));
|
||||
|
||||
// With falling prices, momentum should be negative
|
||||
Assert.False(squeeze.MomentumPositive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_RisingMomentum_Detected()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Flat then accelerating up
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + i * 60000, 100, 101, 99, 100, 1000));
|
||||
}
|
||||
|
||||
// Strong up move
|
||||
squeeze.Update(new TBar(baseTime + 3 * 60000, 100, 115, 99, 112, 1000));
|
||||
squeeze.Update(new TBar(baseTime + 4 * 60000, 112, 125, 110, 122, 1000));
|
||||
|
||||
Assert.True(squeeze.MomentumRising);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Color Coding Tests
|
||||
|
||||
[Fact]
|
||||
public void ColorCode_RisingAboveZero_IsCyan()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Strong uptrend with rising momentum
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + i * 60000, 100 + i * 2, 105 + i * 2, 98 + i * 2, 103 + i * 2, 1000));
|
||||
}
|
||||
|
||||
// Should be MomentumPositive and MomentumRising = ColorCode 0 (Cyan)
|
||||
if (squeeze.MomentumPositive && squeeze.MomentumRising)
|
||||
{
|
||||
Assert.Equal(0, squeeze.ColorCode);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorCode_FallingBelowZero_IsRed()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Strong downtrend with falling momentum
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + i * 60000, 100 - i * 2, 102 - i * 2, 95 - i * 2, 97 - i * 2, 1000));
|
||||
}
|
||||
|
||||
// Should be !MomentumPositive and !MomentumRising = ColorCode 2 (Red)
|
||||
if (!squeeze.MomentumPositive && !squeeze.MomentumRising)
|
||||
{
|
||||
Assert.Equal(2, squeeze.ColorCode);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bar Correction Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_BarCorrection_RestoresPreviousState()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + i * 60000, 100, 105, 95, 102, 1000));
|
||||
}
|
||||
|
||||
// Add new bar
|
||||
squeeze.Update(new TBar(baseTime + 3 * 60000, 100, 110, 98, 108, 1000), isNew: true);
|
||||
double valueAfterNew = squeeze.Momentum.Value;
|
||||
|
||||
// Correct the bar with different data
|
||||
squeeze.Update(new TBar(baseTime + 3 * 60000, 108, 112, 105, 92, 1000), isNew: false);
|
||||
double valueAfterCorrection = squeeze.Momentum.Value;
|
||||
|
||||
Assert.NotEqual(valueAfterNew, valueAfterCorrection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleCorrections_ProduceConsistentResults()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + i * 60000, 100, 105, 95, 102, 1000));
|
||||
}
|
||||
|
||||
// New bar
|
||||
squeeze.Update(new TBar(baseTime + 3 * 60000, 100, 110, 98, 108, 1000), isNew: true);
|
||||
double firstValue = squeeze.Momentum.Value;
|
||||
|
||||
// Correction 1
|
||||
squeeze.Update(new TBar(baseTime + 3 * 60000, 108, 115, 105, 90, 1000), isNew: false);
|
||||
|
||||
// Correction 2 - same as first new bar
|
||||
squeeze.Update(new TBar(baseTime + 3 * 60000, 100, 110, 98, 108, 1000), isNew: false);
|
||||
double secondValue = squeeze.Momentum.Value;
|
||||
|
||||
Assert.Equal(firstValue, secondValue, Precision);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNInput_UsesLastValidValue()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
squeeze.Update(new TBar(baseTime, 100, 105, 95, 102, 1000));
|
||||
|
||||
squeeze.Update(new TBar(baseTime + 60000, double.NaN, double.NaN, double.NaN, double.NaN, 1000));
|
||||
|
||||
Assert.True(double.IsFinite(squeeze.Momentum.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityInput_UsesLastValidValue()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
squeeze.Update(new TBar(baseTime, 100, 105, 95, 102, 1000));
|
||||
|
||||
squeeze.Update(new TBar(baseTime + 60000, double.PositiveInfinity, 105, 95, 102, 1000));
|
||||
|
||||
Assert.True(double.IsFinite(squeeze.Momentum.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + i * 60000, 100, 105, 95, 102, 1000));
|
||||
}
|
||||
|
||||
Assert.True(squeeze.IsHot);
|
||||
|
||||
squeeze.Reset();
|
||||
|
||||
Assert.False(squeeze.IsHot);
|
||||
Assert.Equal(0, squeeze.Momentum.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsFreshStart()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Uptrend
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + i * 60000, 100 + i * 2, 105 + i * 2, 95 + i * 2, 103 + i * 2, 1000));
|
||||
}
|
||||
|
||||
double upTrendMomentum = squeeze.Momentum.Value;
|
||||
|
||||
squeeze.Reset();
|
||||
|
||||
// Downtrend
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + i * 60000, 100 - i * 2, 102 - i * 2, 95 - i * 2, 97 - i * 2, 1000));
|
||||
}
|
||||
|
||||
Assert.NotEqual(upTrendMomentum, squeeze.Momentum.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prime Tests
|
||||
|
||||
[Fact]
|
||||
public void Prime_FillsBuffer()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 5, bbMult: 2.0, kcPeriod: 5, kcMult: 1.5, momPeriod: 5);
|
||||
var source = new TBarSeries();
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
source.Add(new TBar(baseTime + i * 60000, 100, 105, 95, 102, 1000));
|
||||
}
|
||||
|
||||
squeeze.Prime(source);
|
||||
|
||||
Assert.True(squeeze.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_ReturnsSeriesOfCorrectLength()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
source.Add(new TBar(baseTime + i * 60000, 100 + i, 105 + i, 95 + i, 102 + i, 1000));
|
||||
}
|
||||
|
||||
var result = TtmSqueeze.Batch(source);
|
||||
|
||||
Assert.Equal(20, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptySource_ReturnsEmpty()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
var result = TtmSqueeze.Batch(source);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsBothResultsAndIndicator()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
source.Add(new TBar(baseTime + i * 60000, 100, 105, 95, 102, 1000));
|
||||
}
|
||||
|
||||
var (results, indicator) = TtmSqueeze.Calculate(source, bbPeriod: 10, bbMult: 2.0, kcPeriod: 10, kcMult: 1.5, momPeriod: 10);
|
||||
|
||||
Assert.Equal(20, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(10, indicator.BbPeriod);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Publishing Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_PublishesEvent()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
int eventCount = 0;
|
||||
squeeze.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
squeeze.Update(new TBar(baseTime, 100, 105, 95, 102, 1000));
|
||||
|
||||
Assert.Equal(1, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_EventContainsCorrectValue()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
TValue? receivedValue = null;
|
||||
squeeze.Pub += (object? sender, in TValueEventArgs args) => receivedValue = args.Value;
|
||||
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
squeeze.Update(new TBar(baseTime, 100, 105, 95, 102, 1000));
|
||||
|
||||
Assert.NotNull(receivedValue);
|
||||
Assert.Equal(squeeze.Momentum.Value, receivedValue.Value.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GBM Random Data Test
|
||||
|
||||
[Fact]
|
||||
public void Update_GbmData_ProducesFiniteValues()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 14, bbMult: 2.0, kcPeriod: 14, kcMult: 1.5, momPeriod: 14);
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
squeeze.Update(bars[i]);
|
||||
|
||||
// Momentum should always be finite
|
||||
Assert.True(double.IsFinite(squeeze.Momentum.Value));
|
||||
|
||||
// ColorCode should be valid (0-3)
|
||||
Assert.InRange(squeeze.ColorCode, 0, 3);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for TTM Squeeze against known values and mathematical properties.
|
||||
/// </summary>
|
||||
public class TtmSqueezeValidationTests
|
||||
{
|
||||
private const double Precision = 1e-10;
|
||||
|
||||
#region Squeeze Detection Validation
|
||||
|
||||
[Fact]
|
||||
public void SqueezeOn_TightRangeBars_BbInsideKc()
|
||||
{
|
||||
// When price range is very tight, BB bands should contract faster than KC
|
||||
// because BB uses stddev while KC uses ATR (which has minimum = high - low)
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Very tight range bars - stddev will be near 0
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + (i * 60000), 100.0, 100.01, 99.99, 100.0, 1000));
|
||||
}
|
||||
|
||||
// With effectively zero stddev, BB bands collapse to the mean
|
||||
// KC still has some width from ATR (at least the bar range)
|
||||
// This should trigger squeeze on
|
||||
// Note: Due to warmup compensation, exact behavior may vary
|
||||
Assert.True(squeeze.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Momentum_PriceEqualsMidline_ZeroDeviation()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Price bars where close is always at the center of the range
|
||||
// Donchian midline = (high + low) / 2, and close = midline
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
double high = 105;
|
||||
double low = 95;
|
||||
double close = (high + low) / 2; // exactly at midline
|
||||
squeeze.Update(new TBar(baseTime + (i * 60000), 100, high, low, close, 1000));
|
||||
}
|
||||
|
||||
// Momentum should be near zero since price = midline
|
||||
Assert.True(Math.Abs(squeeze.Momentum.Value) < 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Momentum_PriceAboveMidline_PositiveDeviation()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Price bars where close is moving above the donchian midline
|
||||
// Start with balanced range, then consistently close near high
|
||||
squeeze.Update(new TBar(baseTime, 100, 110, 90, 100, 1000)); // midline = 100
|
||||
squeeze.Update(new TBar(baseTime + 60000, 100, 110, 90, 105, 1000)); // close above mid
|
||||
squeeze.Update(new TBar(baseTime + 120000, 105, 110, 90, 108, 1000)); // close above mid
|
||||
squeeze.Update(new TBar(baseTime + 180000, 108, 110, 90, 110, 1000)); // close at high
|
||||
squeeze.Update(new TBar(baseTime + 240000, 110, 112, 88, 112, 1000)); // close at high
|
||||
|
||||
// After warmup, momentum should reflect price above midline (100)
|
||||
Assert.True(squeeze.IsHot);
|
||||
// Momentum reflects deviation from donchian midline regressed
|
||||
// With close consistently above midline, MomentumPositive should be true
|
||||
Assert.True(squeeze.MomentumPositive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Momentum_PriceBelowMidline_NegativeDeviation()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Price bars where close is moving below the donchian midline
|
||||
// Start with balanced range, then consistently close near low
|
||||
squeeze.Update(new TBar(baseTime, 100, 110, 90, 100, 1000)); // midline = 100
|
||||
squeeze.Update(new TBar(baseTime + 60000, 100, 110, 90, 95, 1000)); // close below mid
|
||||
squeeze.Update(new TBar(baseTime + 120000, 95, 110, 90, 92, 1000)); // close below mid
|
||||
squeeze.Update(new TBar(baseTime + 180000, 92, 110, 90, 90, 1000)); // close at low
|
||||
squeeze.Update(new TBar(baseTime + 240000, 90, 112, 88, 88, 1000)); // close at low
|
||||
|
||||
// After warmup, momentum should reflect price below midline (100)
|
||||
Assert.True(squeeze.IsHot);
|
||||
// With close consistently below midline, MomentumPositive should be false
|
||||
Assert.False(squeeze.MomentumPositive);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Linear Regression Validation
|
||||
|
||||
[Fact]
|
||||
public void Momentum_LinearDeviation_CorrectSlope()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 5, bbMult: 2.0, kcPeriod: 5, kcMult: 1.5, momPeriod: 5);
|
||||
long baseTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Create bars where deviation from midline increases linearly
|
||||
// This tests the linear regression component
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
// Fixed range, but close moves away from midline
|
||||
double high = 110;
|
||||
double low = 90;
|
||||
double midline = 100; // (110 + 90) / 2
|
||||
double close = midline + (i * 2); // 100, 102, 104, ...
|
||||
|
||||
squeeze.Update(new TBar(baseTime + (i * 60000), 100, high, low, close, 1000));
|
||||
}
|
||||
|
||||
// Momentum should be strongly positive with rising trend
|
||||
Assert.True(squeeze.Momentum.Value > 10);
|
||||
Assert.True(squeeze.MomentumRising);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Color Coding Validation
|
||||
|
||||
[Fact]
|
||||
public void ColorCode_AllFourStates_AreReachable()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
var colorsSeen = new System.Collections.Generic.HashSet<int>();
|
||||
|
||||
// Uptrend (rising above zero - cyan = 0)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + (i * 60000), 100 + (i * 2), 105 + (i * 2), 95 + (i * 2), 103 + (i * 2), 1000));
|
||||
colorsSeen.Add(squeeze.ColorCode);
|
||||
}
|
||||
|
||||
// Now weakening but still positive (falling above zero - blue = 1)
|
||||
for (int i = 5; i < 10; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + (i * 60000), 115, 118, 112, 114, 1000));
|
||||
colorsSeen.Add(squeeze.ColorCode);
|
||||
}
|
||||
|
||||
// Downtrend (falling below zero - red = 2)
|
||||
for (int i = 10; i < 15; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + (i * 60000), 100 - ((i - 10) * 3), 102 - ((i - 10) * 3), 95 - ((i - 10) * 3), 97 - ((i - 10) * 3), 1000));
|
||||
colorsSeen.Add(squeeze.ColorCode);
|
||||
}
|
||||
|
||||
// Recovering but still negative (rising below zero - yellow = 3)
|
||||
for (int i = 15; i < 20; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + (i * 60000), 80, 85, 78, 82, 1000));
|
||||
colorsSeen.Add(squeeze.ColorCode);
|
||||
}
|
||||
|
||||
// During a varied price series, we should see at least some color variety
|
||||
Assert.True(colorsSeen.Count >= 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorCode_Cyan_WhenRisingAboveZero()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Strong uptrend to ensure positive and rising momentum
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + (i * 60000), 100 + (i * 5), 105 + (i * 5), 95 + (i * 5), 103 + (i * 5), 1000));
|
||||
}
|
||||
|
||||
if (squeeze.MomentumPositive && squeeze.MomentumRising)
|
||||
{
|
||||
Assert.Equal(0, squeeze.ColorCode); // Cyan
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorCode_Red_WhenFallingBelowZero()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Strong downtrend to ensure negative and falling momentum
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + (i * 60000), 100 - (i * 5), 105 - (i * 5), 95 - (i * 5), 97 - (i * 5), 1000));
|
||||
}
|
||||
|
||||
if (!squeeze.MomentumPositive && !squeeze.MomentumRising)
|
||||
{
|
||||
Assert.Equal(2, squeeze.ColorCode); // Red
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Squeeze Fired Validation
|
||||
|
||||
[Fact]
|
||||
public void SqueezeFired_TransitionFromOnToOff_Detected()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 3, bbMult: 2.0, kcPeriod: 3, kcMult: 1.5, momPeriod: 3);
|
||||
long baseTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
int squeezeFiredCount = 0;
|
||||
|
||||
// Start with tight range to build squeeze
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + (i * 60000), 100, 100.1, 99.9, 100, 1000));
|
||||
if (squeeze.SqueezeFired)
|
||||
{
|
||||
squeezeFiredCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Then sudden expansion
|
||||
for (int i = 5; i < 10; i++)
|
||||
{
|
||||
double volatility = (i - 4) * 5;
|
||||
squeeze.Update(new TBar(baseTime + (i * 60000), 100, 100 + volatility, 100 - volatility, 100 + volatility - 2, 1000));
|
||||
if (squeeze.SqueezeFired)
|
||||
{
|
||||
squeezeFiredCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// SqueezeFired should occur at most once per transition
|
||||
// Count tracks any transitions that occurred
|
||||
Assert.True(squeezeFiredCount >= 0, "SqueezeFired should be trackable");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch vs Streaming Consistency
|
||||
|
||||
[Fact]
|
||||
public void Batch_MatchesStreaming_IdenticalResults()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
long baseTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double price = 100 + (Math.Sin(i * 0.2) * 10);
|
||||
double high = price + 2;
|
||||
double low = price - 2;
|
||||
source.Add(new TBar(baseTime + (i * 60000), price, high, low, price + 0.5, 1000));
|
||||
}
|
||||
|
||||
// Batch calculation
|
||||
var (batchResults, _) = TtmSqueeze.Calculate(source, bbPeriod: 10, bbMult: 2.0, kcPeriod: 10, kcMult: 1.5, momPeriod: 10);
|
||||
|
||||
// Streaming calculation
|
||||
var streaming = new TtmSqueeze(bbPeriod: 10, bbMult: 2.0, kcPeriod: 10, kcMult: 1.5, momPeriod: 10);
|
||||
var streamingResults = new System.Collections.Generic.List<double>();
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streaming.Update(source[i], isNew: true);
|
||||
streamingResults.Add(streaming.Momentum.Value);
|
||||
}
|
||||
|
||||
// Results should match
|
||||
Assert.Equal(source.Count, batchResults.Count);
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResults[i].Value, Precision);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void Update_SingleBar_ProducesFiniteOutput()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 20, bbMult: 2.0, kcPeriod: 20, kcMult: 1.5, momPeriod: 20);
|
||||
long baseTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
squeeze.Update(new TBar(baseTime, 100, 105, 95, 102, 1000));
|
||||
|
||||
Assert.True(double.IsFinite(squeeze.Momentum.Value));
|
||||
Assert.False(squeeze.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantPrice_ZeroVariance()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 5, bbMult: 2.0, kcPeriod: 5, kcMult: 1.5, momPeriod: 5);
|
||||
long baseTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// All bars identical
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
squeeze.Update(new TBar(baseTime + (i * 60000), 100, 100, 100, 100, 1000));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(squeeze.Momentum.Value));
|
||||
// With constant price, donchian midline = price, so momentum should be near 0
|
||||
Assert.True(Math.Abs(squeeze.Momentum.Value) < 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ExtremeVolatility_HandledGracefully()
|
||||
{
|
||||
var squeeze = new TtmSqueeze(bbPeriod: 5, bbMult: 2.0, kcPeriod: 5, kcMult: 1.5, momPeriod: 5);
|
||||
long baseTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double range = (i + 1) * 100; // Increasing volatility
|
||||
squeeze.Update(new TBar(baseTime + (i * 60000), 100, 100 + range, 100 - range, 100 + (range / 2), 1000));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(squeeze.Momentum.Value));
|
||||
Assert.InRange(squeeze.ColorCode, 0, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TtmSqueeze_Correction_Recomputes()
|
||||
{
|
||||
var ind = new TtmSqueeze();
|
||||
var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
|
||||
|
||||
// Build state well past warmup
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double p = 100.0 + (10.0 * Math.Sin(2.0 * Math.PI * i / 20.0));
|
||||
ind.Update(new TBar(t0.AddMinutes(i), p, p + 2, p - 2, p, 1000), isNew: true);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
var anchorTime = t0.AddMinutes(100);
|
||||
const double anchorClose = 105.5;
|
||||
ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: true);
|
||||
double anchorMomentum = ind.Momentum.Value;
|
||||
|
||||
// Correction with a dramatically different price — Momentum must change
|
||||
ind.Update(new TBar(anchorTime, anchorClose * 10, (anchorClose + 2) * 10, (anchorClose - 2) * 10, anchorClose * 10, 1000), isNew: false);
|
||||
Assert.NotEqual(anchorMomentum, ind.Momentum.Value);
|
||||
|
||||
// Correction back to original price — must exactly restore original Momentum
|
||||
ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: false);
|
||||
Assert.Equal(anchorMomentum, ind.Momentum.Value, 1e-9);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user