Files
Miha Kralj 060649192f 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
2026-03-12 12:34:16 -07:00

732 lines
22 KiB
C#

using System;
using Xunit;
namespace QuanTAlib.Tests;
public class TtmLrcTests
{
private const double Epsilon = 1e-10;
#region Constructor Tests
[Fact]
public void Constructor_DefaultPeriod_SetsTo100()
{
var indicator = new TtmLrc();
Assert.Equal(100, indicator.WarmupPeriod);
Assert.Equal("TtmLrc(100)", indicator.Name);
}
[Fact]
public void Constructor_CustomPeriod_SetsCorrectly()
{
var indicator = new TtmLrc(50);
Assert.Equal(50, indicator.WarmupPeriod);
Assert.Equal("TtmLrc(50)", indicator.Name);
}
[Fact]
public void Constructor_PeriodOfOne_ThrowsException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new TtmLrc(1));
}
[Fact]
public void Constructor_ZeroPeriod_ThrowsException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new TtmLrc(0));
}
[Fact]
public void Constructor_NegativePeriod_ThrowsException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new TtmLrc(-5));
}
#endregion
#region IsHot/Warmup Tests
[Fact]
public void IsHot_BeforeWarmup_ReturnsFalse()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
for (int i = 0; i < 9; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + i), isNew: true);
Assert.False(indicator.IsHot, $"Should not be hot at point {i + 1}");
}
}
[Fact]
public void IsHot_AtExactWarmup_ReturnsTrue()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + i), isNew: true);
}
Assert.True(indicator.IsHot);
}
[Fact]
public void IsHot_AfterWarmup_RemainsTrue()
{
var indicator = new TtmLrc(5);
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + i), isNew: true);
}
Assert.True(indicator.IsHot);
}
#endregion
#region Band Symmetry Tests
[Fact]
public void Bands_Symmetry_Upper1AndLower1EquidistantFromMiddle()
{
var indicator = new TtmLrc(10);
var bars = new GBM(seed: 42).Fetch(15, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 15; i++)
{
indicator.Update(bars.Close[i], isNew: true);
}
double mid = indicator.Midline.Value;
double upper1 = indicator.Upper1.Value;
double lower1 = indicator.Lower1.Value;
double distUp = upper1 - mid;
double distDown = mid - lower1;
Assert.True(Math.Abs(distUp - distDown) < Epsilon, $"Upper1 and Lower1 should be equidistant from middle. Up: {distUp}, Down: {distDown}");
Assert.Equal(indicator.StdDev, distUp, 10);
}
[Fact]
public void Bands_Symmetry_Upper2AndLower2EquidistantFromMiddle()
{
var indicator = new TtmLrc(10);
var bars = new GBM(seed: 42).Fetch(15, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 15; i++)
{
indicator.Update(bars.Close[i], isNew: true);
}
double mid = indicator.Midline.Value;
double upper2 = indicator.Upper2.Value;
double lower2 = indicator.Lower2.Value;
double distUp = upper2 - mid;
double distDown = mid - lower2;
Assert.True(Math.Abs(distUp - distDown) < Epsilon, $"Upper2 and Lower2 should be equidistant from middle. Up: {distUp}, Down: {distDown}");
Assert.Equal(2.0 * indicator.StdDev, distUp, 10);
}
[Fact]
public void Bands_Ordering_UpperGreaterThanMiddleGreaterThanLower()
{
var indicator = new TtmLrc(10);
var bars = new GBM(seed: 42).Fetch(15, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 15; i++)
{
indicator.Update(bars.Close[i], isNew: true);
}
Assert.True(indicator.Upper2.Value >= indicator.Upper1.Value, "Upper2 should be >= Upper1");
Assert.True(indicator.Upper1.Value >= indicator.Midline.Value, "Upper1 should be >= Midline");
Assert.True(indicator.Midline.Value >= indicator.Lower1.Value, "Midline should be >= Lower1");
Assert.True(indicator.Lower1.Value >= indicator.Lower2.Value, "Lower1 should be >= Lower2");
}
#endregion
#region Linear Data Tests
[Fact]
public void LinearData_PerfectTrend_ZeroStdDev()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
// Perfect linear data: y = 100 + 2*x
for (int i = 0; i < 15; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + 2.0 * i), isNew: true);
}
Assert.True(indicator.IsHot);
Assert.True(Math.Abs(indicator.StdDev) < 1e-9, $"StdDev should be 0 for perfect linear data, got {indicator.StdDev}");
Assert.True(Math.Abs(indicator.Slope - 2.0) < 1e-9, $"Slope should be 2.0, got {indicator.Slope}");
Assert.True(Math.Abs(indicator.RSquared - 1.0) < 1e-9, $"R² should be 1.0 for perfect fit, got {indicator.RSquared}");
// All bands should equal midline when StdDev is 0
Assert.Equal(indicator.Midline.Value, indicator.Upper1.Value, 10);
Assert.Equal(indicator.Midline.Value, indicator.Lower1.Value, 10);
Assert.Equal(indicator.Midline.Value, indicator.Upper2.Value, 10);
Assert.Equal(indicator.Midline.Value, indicator.Lower2.Value, 10);
}
[Fact]
public void LinearData_PositiveSlope_SlopeIsPositive()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
for (int i = 0; i < 15; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + 5.0 * i), isNew: true);
}
Assert.True(indicator.Slope > 0, $"Slope should be positive for uptrend, got {indicator.Slope}");
}
[Fact]
public void LinearData_NegativeSlope_SlopeIsNegative()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
for (int i = 0; i < 15; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 - 3.0 * i), isNew: true);
}
Assert.True(indicator.Slope < 0, $"Slope should be negative for downtrend, got {indicator.Slope}");
}
[Fact]
public void FlatData_ZeroSlope()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
for (int i = 0; i < 15; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100), isNew: true);
}
Assert.True(Math.Abs(indicator.Slope) < 1e-10, $"Slope should be 0 for flat data, got {indicator.Slope}");
Assert.True(Math.Abs(indicator.StdDev) < 1e-10, $"StdDev should be 0 for constant data, got {indicator.StdDev}");
}
#endregion
#region R-Squared Tests
[Fact]
public void RSquared_PerfectFit_EqualsOne()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
for (int i = 0; i < 15; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + 2.0 * i), isNew: true);
}
Assert.True(Math.Abs(indicator.RSquared - 1.0) < 1e-9, $"R² should be 1.0 for perfect linear fit, got {indicator.RSquared}");
}
[Fact]
public void RSquared_RandomData_LessThanOne()
{
var indicator = new TtmLrc(20);
var bars = new GBM(seed: 42).Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 30; i++)
{
indicator.Update(bars.Close[i], isNew: true);
}
Assert.True(indicator.RSquared < 1.0, $"R² should be less than 1.0 for random data, got {indicator.RSquared}");
Assert.True(indicator.RSquared >= 0.0, $"R² should be non-negative, got {indicator.RSquared}");
}
[Fact]
public void RSquared_ClampedBetweenZeroAndOne()
{
var indicator = new TtmLrc(5);
var bars = new GBM(seed: 123).Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 20; i++)
{
indicator.Update(bars.Close[i], isNew: true);
Assert.True(indicator.RSquared >= 0.0 && indicator.RSquared <= 1.0, $"R² should be in [0,1], got {indicator.RSquared}");
}
}
#endregion
#region Bar Correction Tests
[Fact]
public void BarCorrection_IsNewFalse_RevertsToPreviousState()
{
var indicator = new TtmLrc(5);
var now = DateTime.UtcNow;
// Establish base state
for (int i = 0; i < 10; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + i), isNew: true);
}
double originalMid = indicator.Midline.Value;
double originalSlope = indicator.Slope;
double originalStdDev = indicator.StdDev;
// Apply correction with new value
indicator.Update(new TValue(now.AddMinutes(9), 200), isNew: false);
// Should now have different values
Assert.NotEqual(originalMid, indicator.Midline.Value);
// Correct back to original value
indicator.Update(new TValue(now.AddMinutes(9), 100 + 9), isNew: false);
// Should be back to original state
Assert.Equal(originalMid, indicator.Midline.Value, 10);
Assert.Equal(originalSlope, indicator.Slope, 10);
Assert.Equal(originalStdDev, indicator.StdDev, 10);
}
[Fact]
public void BarCorrection_MultipleCorrections_MaintainsConsistentBase()
{
var indicator = new TtmLrc(5);
var now = DateTime.UtcNow;
for (int i = 0; i < 8; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + i * 2), isNew: true);
}
double baseMid = indicator.Midline.Value;
// Multiple corrections
for (int j = 0; j < 5; j++)
{
indicator.Update(new TValue(now.AddMinutes(7), 150 + j * 10), isNew: false);
}
// Revert to original
indicator.Update(new TValue(now.AddMinutes(7), 100 + 7 * 2), isNew: false);
Assert.Equal(baseMid, indicator.Midline.Value, 10);
}
[Fact]
public void BarCorrection_AfterCorrection_NextNewBarUsesCorrectedState()
{
var indicator = new TtmLrc(5);
var now = DateTime.UtcNow;
for (int i = 0; i < 6; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + i), isNew: true);
}
// Correct last bar
indicator.Update(new TValue(now.AddMinutes(5), 150), isNew: false);
// Verify correction applied
Assert.True(indicator.Midline.Value > 100, "Midline should reflect corrected spike value");
// Add new bar
indicator.Update(new TValue(now.AddMinutes(6), 160), isNew: true);
// Verify the correction persisted - the new state should be based on the corrected value
// By checking slope direction changed due to spike
Assert.True(indicator.Slope > 0, "Slope should be positive after spike correction");
}
#endregion
#region Batch vs Streaming Consistency
[Fact]
public void BatchVsStreaming_SameResults()
{
var streamingIndicator = new TtmLrc(20);
int count = 50;
var bars = new GBM(seed: 42).Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < count; i++)
{
streamingIndicator.Update(bars.Close[i], isNew: true);
}
var source = bars.Close;
var (bMid, bU1, bL1, bU2, bL2) = TtmLrc.Batch(source, 20);
// Compare streaming final values to batch final values
Assert.Equal(streamingIndicator.Midline.Value, bMid.Values[^1], 10);
Assert.Equal(streamingIndicator.Upper1.Value, bU1.Values[^1], 10);
Assert.Equal(streamingIndicator.Lower1.Value, bL1.Values[^1], 10);
Assert.Equal(streamingIndicator.Upper2.Value, bU2.Values[^1], 10);
Assert.Equal(streamingIndicator.Lower2.Value, bL2.Values[^1], 10);
}
[Fact]
public void Update_TSeries_ReturnsAllFiveBands()
{
var indicator = new TtmLrc(10);
int count = 20;
var bars = new GBM(seed: 42).Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var source = bars.Close;
var (mid, u1, l1, u2, l2) = indicator.Update(source);
Assert.Equal(count, mid.Count);
Assert.Equal(count, u1.Count);
Assert.Equal(count, l1.Count);
Assert.Equal(count, u2.Count);
Assert.Equal(count, l2.Count);
}
[Fact]
public void Calculate_ReturnsIndicatorAndResults()
{
int count = 30;
var bars = new GBM(seed: 42).Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var source = bars.Close;
var (results, indicator) = TtmLrc.Calculate(source, 15);
Assert.NotNull(indicator);
Assert.True(indicator.IsHot);
Assert.Equal(count, results.Midline.Count);
Assert.Equal(count, results.Upper1.Count);
Assert.Equal(count, results.Lower1.Count);
Assert.Equal(count, results.Upper2.Count);
Assert.Equal(count, results.Lower2.Count);
}
#endregion
#region NaN/Infinity Handling
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var indicator = new TtmLrc(5);
var now = DateTime.UtcNow;
for (int i = 0; i < 5; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + i), isNew: true);
}
// Capture pre-NaN state for verification
Assert.True(double.IsFinite(indicator.Midline.Value), "Midline should be finite before NaN");
// Add NaN
indicator.Update(new TValue(now.AddMinutes(5), double.NaN), isNew: true);
// Should still have valid output (using last valid value)
Assert.True(double.IsFinite(indicator.Midline.Value), "Midline should still be finite after NaN input");
}
[Fact]
public void Infinity_Input_UsesLastValidValue()
{
var indicator = new TtmLrc(5);
var now = DateTime.UtcNow;
for (int i = 0; i < 6; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + i), isNew: true);
}
// Add positive infinity
indicator.Update(new TValue(now.AddMinutes(6), double.PositiveInfinity), isNew: true);
Assert.True(double.IsFinite(indicator.Midline.Value), "Midline should still be finite after Infinity input");
}
[Fact]
public void Batch_NaN_HandledGracefully()
{
var source = new List<double> { 100, 101, double.NaN, 103, 104, 105, 106 };
int len = source.Count;
Span<double> mid = stackalloc double[len];
Span<double> u1 = stackalloc double[len];
Span<double> l1 = stackalloc double[len];
Span<double> u2 = stackalloc double[len];
Span<double> l2 = stackalloc double[len];
TtmLrc.Batch(source.ToArray(), mid, u1, l1, u2, l2, 3);
// All outputs after first few should be finite
for (int i = 2; i < len; i++)
{
Assert.True(double.IsFinite(mid[i]), $"Midline[{i}] should be finite");
}
}
#endregion
#region Reset Tests
[Fact]
public void Reset_ClearsState()
{
var indicator = new TtmLrc(5);
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + i * 2), isNew: true);
}
Assert.True(indicator.IsHot);
Assert.True(indicator.Slope > 0);
indicator.Reset();
Assert.False(indicator.IsHot);
Assert.Equal(0, indicator.Slope);
Assert.Equal(0, indicator.StdDev);
Assert.Equal(0, indicator.RSquared);
}
[Fact]
public void Reset_AllowsReuse()
{
var indicator = new TtmLrc(5);
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + i), isNew: true);
}
double firstRunMid = indicator.Midline.Value;
indicator.Reset();
for (int i = 0; i < 10; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + i), isNew: true);
}
// Results should be identical after reuse
Assert.Equal(firstRunMid, indicator.Midline.Value, 10);
}
#endregion
#region Prime Tests
[Fact]
public void Prime_InitializesFromSeries()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
var times = new List<long>(15);
var values = new List<double>(15);
for (int i = 0; i < 15; i++)
{
times.Add(now.AddMinutes(i).Ticks);
values.Add(100 + i * 2);
}
var source = new TSeries(times, values);
indicator.Prime(source);
Assert.True(indicator.IsHot);
Assert.True(Math.Abs(indicator.Slope - 2.0) < 1e-9);
}
[Fact]
public void Constructor_WithSource_AutoSubscribes()
{
var source = new TSeries();
var indicator = new TtmLrc(source, 5);
var now = DateTime.UtcNow;
for (int i = 0; i < 8; i++)
{
source.Add(new TValue(now.AddMinutes(i), 100 + i * 3), isNew: true);
}
Assert.True(indicator.IsHot);
Assert.True(Math.Abs(indicator.Slope - 3.0) < 1e-9);
}
#endregion
#region Edge Cases
[Fact]
public void EmptySeries_ReturnsEmptyResults()
{
var indicator = new TtmLrc(10);
var source = new TSeries();
var (mid, u1, l1, u2, l2) = indicator.Update(source);
Assert.True(mid.Count == 0, "Midline should be empty for empty source");
Assert.True(u1.Count == 0, "Upper1 should be empty for empty source");
Assert.True(l1.Count == 0, "Lower1 should be empty for empty source");
Assert.True(u2.Count == 0, "Upper2 should be empty for empty source");
Assert.True(l2.Count == 0, "Lower2 should be empty for empty source");
}
[Fact]
public void SingleValue_AllBandsEqual()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
indicator.Update(new TValue(now, 100), isNew: true);
Assert.Equal(100, indicator.Midline.Value);
Assert.Equal(100, indicator.Upper1.Value);
Assert.Equal(100, indicator.Lower1.Value);
Assert.Equal(100, indicator.Upper2.Value);
Assert.Equal(100, indicator.Lower2.Value);
}
[Fact]
public void TwoValues_CalculatesRegression()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
indicator.Update(new TValue(now, 100), isNew: true);
indicator.Update(new TValue(now.AddMinutes(1), 110), isNew: true);
// Slope should be 10 (rise of 10 over run of 1)
Assert.True(Math.Abs(indicator.Slope - 10.0) < 1e-9, $"Slope should be 10, got {indicator.Slope}");
// Midline at x=1 should be 110
Assert.True(Math.Abs(indicator.Midline.Value - 110.0) < 1e-9, $"Midline should be 110, got {indicator.Midline.Value}");
}
[Fact]
public void VerySmallPeriod_Period2_Works()
{
var indicator = new TtmLrc(2);
var now = DateTime.UtcNow;
indicator.Update(new TValue(now, 100), isNew: true);
indicator.Update(new TValue(now.AddMinutes(1), 120), isNew: true);
indicator.Update(new TValue(now.AddMinutes(2), 130), isNew: true);
Assert.True(indicator.IsHot);
Assert.True(double.IsFinite(indicator.Midline.Value));
Assert.True(double.IsFinite(indicator.Slope));
}
[Fact]
public void LargePeriod_HandlesCorrectly()
{
var indicator = new TtmLrc(200);
var bars = new GBM(seed: 42).Fetch(250, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 250; i++)
{
indicator.Update(bars.Close[i], isNew: true);
}
Assert.True(indicator.IsHot);
Assert.True(double.IsFinite(indicator.Midline.Value));
Assert.True(double.IsFinite(indicator.Slope));
Assert.True(indicator.RSquared >= 0 && indicator.RSquared <= 1);
}
#endregion
#region Batch Validation Tests
[Fact]
public void Batch_InvalidPeriod_ThrowsException()
{
double[] source = new double[10];
double[] mid = new double[10];
double[] u1 = new double[10];
double[] l1 = new double[10];
double[] u2 = new double[10];
double[] l2 = new double[10];
Assert.Throws<ArgumentOutOfRangeException>(() =>
TtmLrc.Batch(source, mid, u1, l1, u2, l2, 1));
}
[Fact]
public void Batch_OutputTooShort_ThrowsException()
{
double[] source = new double[10];
double[] mid = new double[5]; // Too short
double[] u1 = new double[10];
double[] l1 = new double[10];
double[] u2 = new double[10];
double[] l2 = new double[10];
Assert.Throws<ArgumentException>(() =>
TtmLrc.Batch(source, mid, u1, l1, u2, l2, 3));
}
#endregion
#region Pub/Sub Tests
[Fact]
public void Pub_FiredOnUpdate()
{
var indicator = new TtmLrc(5);
var now = DateTime.UtcNow;
int eventCount = 0;
void OnPub(object? sender, in TValueEventArgs args) { eventCount = eventCount + 1; }
indicator.Pub += OnPub;
for (int i = 0; i < 8; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + i), isNew: true);
}
Assert.Equal(8, eventCount);
}
[Fact]
public void Pub_ReceivesCorrectValue()
{
var indicator = new TtmLrc(5);
var now = DateTime.UtcNow;
TValue? lastPubValue = null;
void OnPub(object? sender, in TValueEventArgs args) => lastPubValue = args.Value;
indicator.Pub += OnPub;
for (int i = 0; i < 8; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + i * 2), isNew: true);
}
Assert.NotNull(lastPubValue);
Assert.Equal(indicator.Midline.Value, lastPubValue.Value.Value, 10);
}
#endregion
}