mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-03 19:57:44 +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
230 lines
7.2 KiB
C#
230 lines
7.2 KiB
C#
// TtmTrend: Mathematical property validation tests
|
|
// TTM Trend is a proprietary John Carter indicator — no external library equivalents exist.
|
|
// Validation uses mathematical property testing against known EMA behaviors.
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
using Xunit;
|
|
|
|
public class TtmTrendValidationTests
|
|
{
|
|
private const int DefaultPeriod = 6;
|
|
private const int TestDataLength = 500;
|
|
|
|
[Fact]
|
|
public void TtmTrend_EmaOutput_IsFiniteForGbmData()
|
|
{
|
|
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
var ttm = new TtmTrend(DefaultPeriod);
|
|
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
var result = ttm.Update(bars[i], isNew: true);
|
|
Assert.True(double.IsFinite(result.Value),
|
|
$"TtmTrend output must be finite at bar {i}, got {result.Value}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void TtmTrend_TrendDirection_OnlyValidValues()
|
|
{
|
|
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
var ttm = new TtmTrend(DefaultPeriod);
|
|
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
ttm.Update(bars[i], isNew: true);
|
|
|
|
Assert.True(ttm.Trend is -1 or 0 or 1,
|
|
$"Trend must be -1, 0, or 1 at bar {i}, got {ttm.Trend}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void TtmTrend_Strength_IsNonNegative()
|
|
{
|
|
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
var ttm = new TtmTrend(DefaultPeriod);
|
|
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
ttm.Update(bars[i], isNew: true);
|
|
|
|
Assert.True(ttm.Strength >= 0,
|
|
$"Strength must be >= 0 at bar {i}, got {ttm.Strength}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void TtmTrend_RisingSequence_BullishTrend()
|
|
{
|
|
var ttm = new TtmTrend(DefaultPeriod);
|
|
double basePrice = 100.0;
|
|
|
|
// Feed enough bars to warm up, then inject consistently rising prices
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
double price = basePrice + i * 2.0;
|
|
var bar = new TBar(
|
|
DateTime.UtcNow.AddMinutes(i),
|
|
price - 0.5, price + 0.5, price - 0.5, price, 1000);
|
|
ttm.Update(bar, isNew: true);
|
|
}
|
|
|
|
// After a consistently rising sequence, trend should be bullish
|
|
Assert.Equal(1, ttm.Trend);
|
|
}
|
|
|
|
[Fact]
|
|
public void TtmTrend_FallingSequence_BearishTrend()
|
|
{
|
|
var ttm = new TtmTrend(DefaultPeriod);
|
|
double basePrice = 200.0;
|
|
|
|
// Feed enough bars to warm up, then inject consistently falling prices
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
double price = basePrice - i * 2.0;
|
|
var bar = new TBar(
|
|
DateTime.UtcNow.AddMinutes(i),
|
|
price + 0.5, price + 0.5, price - 0.5, price, 1000);
|
|
ttm.Update(bar, isNew: true);
|
|
}
|
|
|
|
// After a consistently falling sequence, trend should be bearish
|
|
Assert.Equal(-1, ttm.Trend);
|
|
}
|
|
|
|
[Fact]
|
|
public void TtmTrend_ConstantPrice_ZeroStrength()
|
|
{
|
|
var ttm = new TtmTrend(DefaultPeriod);
|
|
double price = 100.0;
|
|
|
|
// Feed constant-price bars
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
var bar = new TBar(
|
|
DateTime.UtcNow.AddMinutes(i),
|
|
price, price, price, price, 1000);
|
|
ttm.Update(bar, isNew: true);
|
|
}
|
|
|
|
// Strength should be 0 for a constant series (no percent change)
|
|
Assert.Equal(0.0, ttm.Strength, precision: 10);
|
|
}
|
|
|
|
[Fact]
|
|
public void TtmTrend_EmaConvergesToConstant()
|
|
{
|
|
var ttm = new TtmTrend(DefaultPeriod);
|
|
double targetPrice = 100.0;
|
|
|
|
// Start at 50, abruptly switch to constant 100
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
var bar = new TBar(
|
|
DateTime.UtcNow.AddMinutes(i),
|
|
50, 50, 50, 50, 1000);
|
|
ttm.Update(bar, isNew: true);
|
|
}
|
|
|
|
// Now feed constant 100 for many bars
|
|
for (int i = 5; i < 100; i++)
|
|
{
|
|
var bar = new TBar(
|
|
DateTime.UtcNow.AddMinutes(i),
|
|
targetPrice, targetPrice, targetPrice, targetPrice, 1000);
|
|
ttm.Update(bar, isNew: true);
|
|
}
|
|
|
|
// EMA output should converge to the target price
|
|
Assert.Equal(targetPrice, ttm.Last.Value, precision: 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void TtmTrend_BatchAndStreaming_ProduceSameResults()
|
|
{
|
|
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
|
|
// Batch mode
|
|
var batchResults = TtmTrend.Batch(bars, DefaultPeriod);
|
|
|
|
// Streaming mode
|
|
var streamTtm = new TtmTrend(DefaultPeriod);
|
|
var streamResults = new double[bars.Count];
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
var result = streamTtm.Update(bars[i], isNew: true);
|
|
streamResults[i] = result.Value;
|
|
}
|
|
|
|
// Both must match
|
|
Assert.Equal(batchResults.Count, bars.Count);
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
Assert.Equal(batchResults.Values[i], streamResults[i], precision: 10);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void TtmTrend_DifferentPeriods_ProduceDifferentEmaSmoothing()
|
|
{
|
|
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
|
|
var ttm3 = new TtmTrend(period: 3);
|
|
var ttm20 = new TtmTrend(period: 20);
|
|
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
ttm3.Update(bars[i], isNew: true);
|
|
ttm20.Update(bars[i], isNew: true);
|
|
}
|
|
|
|
// Different periods should produce different final values (except on trivially constant data)
|
|
Assert.NotEqual(ttm3.Last.Value, ttm20.Last.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void TtmTrend_IsHot_AfterWarmup()
|
|
{
|
|
var ttm = new TtmTrend(DefaultPeriod);
|
|
|
|
// First bar: not hot
|
|
var bar1 = new TBar(DateTime.UtcNow, 100, 101, 99, 100, 1000);
|
|
ttm.Update(bar1, isNew: true);
|
|
Assert.False(ttm.IsHot);
|
|
|
|
// Second bar: should be hot (warmup period = 2)
|
|
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 101, 102, 100, 101, 1000);
|
|
ttm.Update(bar2, isNew: true);
|
|
Assert.True(ttm.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void TtmTrend_BarCorrection_IsNewFalse_RestoresState()
|
|
{
|
|
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
var ttm = new TtmTrend(DefaultPeriod);
|
|
|
|
// Process 30 bars
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
ttm.Update(bars[i], isNew: true);
|
|
}
|
|
|
|
_ = ttm.Last.Value;
|
|
|
|
// Update bar 30 (isNew=true) then correct it (isNew=false) with same value
|
|
ttm.Update(bars[30], isNew: true);
|
|
double afterNew = ttm.Last.Value;
|
|
|
|
// Correct with isNew=false using same bar
|
|
ttm.Update(bars[30], isNew: false);
|
|
double afterCorrection = ttm.Last.Value;
|
|
|
|
// Bar correction with same data should produce the same value
|
|
Assert.Equal(afterNew, afterCorrection, precision: 10);
|
|
}
|
|
}
|