mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-05 12:37:43 +00:00
060649192f
- 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
518 lines
16 KiB
C#
518 lines
16 KiB
C#
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
|
|
}
|