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:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
@@ -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
}