mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-19 11:08:05 +00:00
adding missing validations
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class TdSeqIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void TdSeqIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new TdSeqIndicator();
|
||||
|
||||
Assert.Equal(4, indicator.ComparePeriod);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("TD_SEQ - TD Sequential", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TdSeqIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new TdSeqIndicator { ComparePeriod = 4 };
|
||||
|
||||
Assert.Equal(0, TdSeqIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TdSeqIndicator_ShortName_IncludesComparePeriod()
|
||||
{
|
||||
var indicator = new TdSeqIndicator { ComparePeriod = 6 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("TD_SEQ", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("6", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TdSeqIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new TdSeqIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Td_seq.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TdSeqIndicator_Initialize_CreatesTwoLineSeries()
|
||||
{
|
||||
var indicator = new TdSeqIndicator { ComparePeriod = 4 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Setup line + Countdown line
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TdSeqIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TdSeqIndicator { ComparePeriod = 4 };
|
||||
indicator.Initialize();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
double setupValue = indicator.LinesSeries[0].GetValue(0);
|
||||
double countdownValue = indicator.LinesSeries[1].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(setupValue));
|
||||
Assert.True(double.IsFinite(countdownValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TdSeqIndicator_ProcessUpdate_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new TdSeqIndicator { ComparePeriod = 4 };
|
||||
indicator.Initialize();
|
||||
|
||||
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);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.True(indicator.LinesSeries[0].Count >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TdSeqIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new TdSeqIndicator { ComparePeriod = 4 };
|
||||
|
||||
indicator.ComparePeriod = 6;
|
||||
|
||||
Assert.Equal(6, indicator.ComparePeriod);
|
||||
Assert.Equal(0, TdSeqIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TdSeqIndicator_RisingPrices_SetupCountPositive()
|
||||
{
|
||||
var indicator = new TdSeqIndicator { ComparePeriod = 4 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double p = 100.0 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), p, p + 2, p - 2, p);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// After 9+ qualifying bars, setup line should show a positive value
|
||||
double setupValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(setupValue >= 0, $"Expected non-negative setup for rising prices, got {setupValue}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class TdSeqIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Compare Period", sortIndex: 1, 1, 100, 1, 0)]
|
||||
public int ComparePeriod { get; set; } = 4;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private TdSeq _tdSeq = null!;
|
||||
private readonly LineSeries _setupLine;
|
||||
private readonly LineSeries _countdownLine;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"TD_SEQ ({ComparePeriod})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/td_seq/Td_seq.Quantower.cs";
|
||||
|
||||
public TdSeqIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "TD_SEQ - TD Sequential";
|
||||
Description = "Tom DeMark's exhaustion counting system: Setup (±1 to ±9) and Countdown (±1 to ±13) phases detecting trend reversals.";
|
||||
|
||||
_setupLine = new LineSeries("Setup", Color.Yellow, 2, LineStyle.Solid);
|
||||
_countdownLine = new LineSeries("Countdown", Color.Cyan, 1, LineStyle.Solid);
|
||||
|
||||
AddLineSeries(_setupLine);
|
||||
AddLineSeries(_countdownLine);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_tdSeq = new TdSeq(ComparePeriod);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
_ = _tdSeq.Update(this.GetInputBar(args), args.IsNewBar());
|
||||
|
||||
_setupLine.SetValue(_tdSeq.Setup, _tdSeq.IsHot, ShowColdValues);
|
||||
_countdownLine.SetValue(_tdSeq.Countdown, _tdSeq.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class TdSeqTests
|
||||
{
|
||||
private static TBar Bar(double close, double high = 0, double low = 0) =>
|
||||
new(DateTime.UtcNow, open: close, high: high == 0 ? close + 1 : high,
|
||||
low: low == 0 ? close - 1 : low, close: close, volume: 1000);
|
||||
|
||||
private static TBar[] MakeBars(double[] closes)
|
||||
{
|
||||
var bars = new TBar[closes.Length];
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
bars[i] = Bar(closes[i]);
|
||||
}
|
||||
|
||||
return bars;
|
||||
}
|
||||
|
||||
private static TBar[] GbmBars(int count, int seed = 42)
|
||||
{
|
||||
var gbm = new GBM(100.0, 0.02, 0.1, seed: seed);
|
||||
var series = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var bars = new TBar[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
double c = series.Close.Values[i];
|
||||
double h = series.High.Values[i];
|
||||
double l = series.Low.Values[i];
|
||||
bars[i] = new TBar(DateTime.UtcNow.AddMinutes(i), c, h, l, c, 1000);
|
||||
}
|
||||
|
||||
return bars;
|
||||
}
|
||||
|
||||
// ───── A) Constructor validation ─────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroComparePeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new TdSeq(comparePeriod: 0));
|
||||
Assert.Equal("comparePeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeComparePeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new TdSeq(comparePeriod: -1));
|
||||
Assert.Equal("comparePeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Default_SetsProperties()
|
||||
{
|
||||
var td = new TdSeq();
|
||||
Assert.Equal("TdSeq(4)", td.Name);
|
||||
Assert.Equal(5, td.WarmupPeriod);
|
||||
Assert.False(td.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomPeriod_SetsProperties()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 3);
|
||||
Assert.Equal("TdSeq(3)", td.Name);
|
||||
Assert.Equal(4, td.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ───── B) Basic calculation ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var td = new TdSeq();
|
||||
var result = td.Update(Bar(100.0));
|
||||
Assert.IsType<TValue>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Last_IsAccessible()
|
||||
{
|
||||
var td = new TdSeq();
|
||||
td.Update(Bar(100.0));
|
||||
Assert.False(td.IsHot);
|
||||
Assert.Equal("TdSeq(4)", td.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SellSetup_CountsPositive()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 4);
|
||||
// Feed 5 bars to get IsHot, then continue rising
|
||||
// Rising closes: close > close[4] for consecutive bars → sell setup
|
||||
double[] prices = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110];
|
||||
foreach (double p in prices)
|
||||
{
|
||||
td.Update(Bar(p));
|
||||
}
|
||||
|
||||
Assert.True(td.IsHot);
|
||||
Assert.True(td.Setup > 0, $"Expected positive setup, got {td.Setup}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BuySetup_CountsNegative()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 4);
|
||||
// Falling closes: close < close[4] → buy setup (negative)
|
||||
double[] prices = [110, 109, 108, 107, 106, 105, 104, 103, 102, 101, 100];
|
||||
foreach (double p in prices)
|
||||
{
|
||||
td.Update(Bar(p));
|
||||
}
|
||||
|
||||
Assert.True(td.IsHot);
|
||||
Assert.True(td.Setup < 0, $"Expected negative setup, got {td.Setup}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SetupComplete_ReachesNine()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 4);
|
||||
// Steadily rising for 13+ bars (9 qualify for sell setup after 4-bar lookback)
|
||||
// Bars 0-3: prime the history. Bars 4-12: each > close[4] → consecutive sell setup
|
||||
double[] prices = new double[20];
|
||||
for (int i = 0; i < 20; i++) { prices[i] = 100.0 + i; }
|
||||
|
||||
foreach (double p in prices)
|
||||
{
|
||||
td.Update(Bar(p));
|
||||
}
|
||||
|
||||
// After 9 consecutive qualifying bars setup should have been clamped to 9
|
||||
Assert.Equal(9, td.Setup);
|
||||
}
|
||||
|
||||
// ───── C) State + bar correction ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_True_AdvancesState()
|
||||
{
|
||||
var td = new TdSeq();
|
||||
td.Update(Bar(100.0), isNew: true);
|
||||
_ = td.Setup; // capture state after first update
|
||||
td.Update(Bar(200.0), isNew: true);
|
||||
// Second bar may have different setup due to price change
|
||||
Assert.False(td.IsHot); // still warming up
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_False_IsIdempotent()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 4);
|
||||
double[] prices = [100, 101, 102, 103, 104, 105, 106];
|
||||
foreach (double p in prices)
|
||||
{
|
||||
td.Update(Bar(p), isNew: true);
|
||||
}
|
||||
|
||||
// Correct last bar twice — same result
|
||||
td.Update(Bar(106.5), isNew: false);
|
||||
double v1 = td.Last.Value;
|
||||
td.Update(Bar(106.5), isNew: false);
|
||||
double v2 = td.Last.Value;
|
||||
|
||||
Assert.Equal(v1, v2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_Restore()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 4);
|
||||
double[] prices = [100, 101, 102, 103, 104, 105, 106];
|
||||
foreach (double p in prices)
|
||||
{
|
||||
td.Update(Bar(p), isNew: true);
|
||||
}
|
||||
|
||||
double baseline = td.Last.Value;
|
||||
|
||||
// Correct to various prices then back to original
|
||||
td.Update(Bar(999.0), isNew: false);
|
||||
td.Update(Bar(50.0), isNew: false);
|
||||
td.Update(Bar(106.0), isNew: false);
|
||||
|
||||
Assert.Equal(baseline, td.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsAllState()
|
||||
{
|
||||
var td = new TdSeq();
|
||||
double[] bars = new double[30];
|
||||
for (int i = 0; i < 30; i++) { bars[i] = 100.0 + i; }
|
||||
foreach (double p in bars)
|
||||
{
|
||||
td.Update(Bar(p));
|
||||
}
|
||||
|
||||
Assert.True(td.IsHot);
|
||||
|
||||
td.Reset();
|
||||
|
||||
Assert.False(td.IsHot);
|
||||
Assert.Equal(0, td.Setup);
|
||||
Assert.Equal(0, td.Countdown);
|
||||
Assert.Equal(default, td.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ThenReFeed_GivesSameResult()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 4);
|
||||
var bars = MakeBars([100, 101, 102, 103, 104, 105, 106, 107]);
|
||||
|
||||
foreach (var b in bars) { td.Update(b); }
|
||||
double first = td.Last.Value;
|
||||
|
||||
td.Reset();
|
||||
foreach (var b in bars) { td.Update(b); }
|
||||
double second = td.Last.Value;
|
||||
|
||||
Assert.Equal(first, second);
|
||||
}
|
||||
|
||||
// ───── D) Warmup / convergence ─────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FalseBeforeEnoughBars()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 4);
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
td.Update(Bar(100.0 + i));
|
||||
Assert.False(td.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_TrueAfterWarmupPeriod()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 4);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
td.Update(Bar(100.0 + i));
|
||||
}
|
||||
|
||||
Assert.True(td.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsComparePeriodPlusOne()
|
||||
{
|
||||
Assert.Equal(5, new TdSeq(4).WarmupPeriod);
|
||||
Assert.Equal(4, new TdSeq(3).WarmupPeriod);
|
||||
Assert.Equal(2, new TdSeq(1).WarmupPeriod);
|
||||
}
|
||||
|
||||
// ───── E) Robustness ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_Close_UsesLastValid()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 4);
|
||||
var bars = GbmBars(10);
|
||||
foreach (var b in bars) { td.Update(b); }
|
||||
|
||||
td.Update(new TBar(DateTime.UtcNow, 100, 110, 90, double.NaN, 1000));
|
||||
Assert.True(double.IsFinite(td.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_Close_UsesLastValid()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 4);
|
||||
var bars = GbmBars(10);
|
||||
foreach (var b in bars) { td.Update(b); }
|
||||
|
||||
td.Update(new TBar(DateTime.UtcNow, 100, 110, 90, double.PositiveInfinity, 1000));
|
||||
Assert.True(double.IsFinite(td.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BatchNaN_Safe()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 4);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
td.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 1000));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(td.Last.Value));
|
||||
}
|
||||
|
||||
// ───── F) Consistency (streaming == eventing) ─────
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResults()
|
||||
{
|
||||
int count = 200;
|
||||
var gbm = new GBM(100.0, 0.02, 0.1, seed: 77);
|
||||
var tbarSeries = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var bars = new TBar[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
bars[i] = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
tbarSeries.Close.Values[i],
|
||||
tbarSeries.High.Values[i],
|
||||
tbarSeries.Low.Values[i],
|
||||
tbarSeries.Close.Values[i],
|
||||
1000);
|
||||
}
|
||||
|
||||
// 1. Streaming
|
||||
var streaming = new TdSeq(4);
|
||||
var streamResults = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(bars[i]).Value;
|
||||
}
|
||||
|
||||
// 2. Event-based via TBarSeries
|
||||
var barSource = new TBarSeries();
|
||||
var eventIndicator = new TdSeq(barSource, 4);
|
||||
var eventResults = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
barSource.Add(bars[i]);
|
||||
eventResults[i] = eventIndicator.Last.Value;
|
||||
}
|
||||
|
||||
// Compare all
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], eventResults[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// ───── G) Countdown phase ─────
|
||||
|
||||
[Fact]
|
||||
public void Countdown_StartsAfterSetupCompletes()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 4);
|
||||
// Need 9 consecutive qualifying sell-setup bars after warmup
|
||||
// Warmup = 4 bars, then 9 more bars where close > close[4]
|
||||
double[] prices = new double[30];
|
||||
for (int i = 0; i < 30; i++) { prices[i] = 100.0 + i; }
|
||||
|
||||
foreach (double p in prices)
|
||||
{
|
||||
td.Update(Bar(p, high: p + 2, low: p - 2));
|
||||
}
|
||||
|
||||
// After 9+ qualifying bars, setup should complete and countdown may be active
|
||||
// Setup is clamped at 9, countdown starts at 0 and increments when conditions met
|
||||
Assert.Equal(9, td.Setup); // setup stays at 9 (clamped)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetupCount_ResetWhenDirectionFlips()
|
||||
{
|
||||
var td = new TdSeq(comparePeriod: 4);
|
||||
// First go up (sell setup)
|
||||
double[] rising = [100, 101, 102, 103, 104, 105, 106, 107];
|
||||
foreach (double p in rising) { td.Update(Bar(p)); }
|
||||
Assert.True(td.Setup > 0);
|
||||
|
||||
// Then go sharply down (buy setup)
|
||||
double[] falling = [80, 79, 78, 77, 76, 75, 74, 73];
|
||||
foreach (double p in falling) { td.Update(Bar(p)); }
|
||||
Assert.True(td.Setup < 0, $"Expected negative setup after reversal, got {td.Setup}");
|
||||
}
|
||||
|
||||
// ───── H) Chainability ─────
|
||||
|
||||
[Fact]
|
||||
public void PubEvent_FiresOnUpdate()
|
||||
{
|
||||
var td = new TdSeq();
|
||||
int firedCount = 0;
|
||||
td.Pub += (object? _, in TValueEventArgs _) => firedCount++;
|
||||
|
||||
td.Update(Bar(100.0));
|
||||
Assert.Equal(1, firedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventChaining_Works()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
var td = new TdSeq(source, comparePeriod: 4);
|
||||
var downstream = new TSeries();
|
||||
td.Pub += (object? _, in TValueEventArgs e) => downstream.Add(e.Value);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
source.Add(Bar(100.0 + i));
|
||||
}
|
||||
|
||||
Assert.Equal(10, downstream.Count);
|
||||
}
|
||||
|
||||
// ───── Calculate ─────
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsFullSeries()
|
||||
{
|
||||
var gbm = new GBM(100.0, 0.02, 0.1, seed: 42);
|
||||
var tbarSeries = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
int count = 50;
|
||||
var barSeries = new TBarSeries();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
barSeries.Add(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
tbarSeries.Close.Values[i],
|
||||
tbarSeries.High.Values[i],
|
||||
tbarSeries.Low.Values[i],
|
||||
tbarSeries.Close.Values[i],
|
||||
1000));
|
||||
}
|
||||
|
||||
TSeries results = TdSeq.Calculate(barSeries, comparePeriod: 4);
|
||||
Assert.Equal(count, results.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_MatchesStreaming()
|
||||
{
|
||||
int count = 100;
|
||||
var gbm = new GBM(100.0, 0.02, 0.1, seed: 7);
|
||||
var tbarSeries = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var bars = new TBar[count];
|
||||
var barSeries = new TBarSeries();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
bars[i] = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
tbarSeries.Close.Values[i],
|
||||
tbarSeries.High.Values[i],
|
||||
tbarSeries.Low.Values[i],
|
||||
tbarSeries.Close.Values[i],
|
||||
1000);
|
||||
barSeries.Add(bars[i]);
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var streaming = new TdSeq(4);
|
||||
var streamResults = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(bars[i]).Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
TSeries batchResults = TdSeq.Calculate(barSeries, comparePeriod: 4);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// TD_SEQ Validation Tests — self-consistency only (no external library equivalent).
|
||||
/// Validates: streaming == batch, determinism, NaN safety, direction reversal logic.
|
||||
/// </summary>
|
||||
public sealed class TdSeqValidationTests
|
||||
{
|
||||
private static TBar[] MakeBars(int count, int seed = 42)
|
||||
{
|
||||
var gbm = new GBM(100.0, 0.02, 0.1, seed: seed);
|
||||
var tbarSeries = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var bars = new TBar[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
bars[i] = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
tbarSeries.Close.Values[i],
|
||||
tbarSeries.High.Values[i],
|
||||
tbarSeries.Low.Values[i],
|
||||
tbarSeries.Close.Values[i],
|
||||
1000);
|
||||
}
|
||||
|
||||
return bars;
|
||||
}
|
||||
|
||||
// ─── Self-consistency: streaming == batch ───
|
||||
|
||||
[Fact]
|
||||
public void Streaming_EqualsBatch_Period4()
|
||||
{
|
||||
var bars = MakeBars(500);
|
||||
var barSeries = new TBarSeries();
|
||||
foreach (var b in bars) { barSeries.Add(b); }
|
||||
|
||||
// Streaming
|
||||
var streaming = new TdSeq(4);
|
||||
var streamResults = new double[bars.Length];
|
||||
for (int i = 0; i < bars.Length; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(bars[i]).Value;
|
||||
}
|
||||
|
||||
// Batch via Calculate
|
||||
TSeries batchResults = TdSeq.Calculate(barSeries, 4);
|
||||
|
||||
for (int i = 0; i < bars.Length; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Streaming_EqualsBatch_Period2()
|
||||
{
|
||||
var bars = MakeBars(200, seed: 13);
|
||||
var barSeries = new TBarSeries();
|
||||
foreach (var b in bars) { barSeries.Add(b); }
|
||||
|
||||
var streaming = new TdSeq(2);
|
||||
var streamResults = new double[bars.Length];
|
||||
for (int i = 0; i < bars.Length; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(bars[i]).Value;
|
||||
}
|
||||
|
||||
TSeries batchResults = TdSeq.Calculate(barSeries, 2);
|
||||
|
||||
for (int i = 0; i < bars.Length; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Determinism: same input → same output ───
|
||||
|
||||
[Fact]
|
||||
public void Determinism_SameSeed_SameResults()
|
||||
{
|
||||
var bars1 = MakeBars(100, seed: 99);
|
||||
var bars2 = MakeBars(100, seed: 99);
|
||||
|
||||
var td1 = new TdSeq(4);
|
||||
var td2 = new TdSeq(4);
|
||||
|
||||
for (int i = 0; i < bars1.Length; i++)
|
||||
{
|
||||
double v1 = td1.Update(bars1[i]).Value;
|
||||
double v2 = td2.Update(bars2[i]).Value;
|
||||
Assert.Equal(v1, v2);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Known-value spot check ───
|
||||
|
||||
[Fact]
|
||||
public void SellSetup_PureRising_CountsCorrectly()
|
||||
{
|
||||
// Pure monotone rising: bars 0-3 prime, bars 4-12 each qualify as sell setup
|
||||
// After 9 qualifying bars the setup count clamps to 9
|
||||
var td = new TdSeq(4);
|
||||
int maxSetup = 0;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double p = 100.0 + i;
|
||||
td.Update(new TBar(DateTime.UtcNow.AddMinutes(i), p, p + 2, p - 2, p, 1000));
|
||||
if (td.Setup > maxSetup) { maxSetup = td.Setup; }
|
||||
}
|
||||
|
||||
Assert.Equal(9, maxSetup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuySetup_PureFalling_CountsNegativeNine()
|
||||
{
|
||||
var td = new TdSeq(4);
|
||||
int minSetup = 0;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double p = 200.0 - i;
|
||||
td.Update(new TBar(DateTime.UtcNow.AddMinutes(i), p, p + 2, p - 2, p, 1000));
|
||||
if (td.Setup < minSetup) { minSetup = td.Setup; }
|
||||
}
|
||||
|
||||
Assert.Equal(-9, minSetup);
|
||||
}
|
||||
|
||||
// ─── Setup clamp: never exceeds ±9 ───
|
||||
|
||||
[Fact]
|
||||
public void Setup_NeverExceedsNine()
|
||||
{
|
||||
var bars = MakeBars(500, seed: 7);
|
||||
var td = new TdSeq(4);
|
||||
foreach (var b in bars)
|
||||
{
|
||||
td.Update(b);
|
||||
Assert.True(td.Setup >= -9 && td.Setup <= 9,
|
||||
$"Setup {td.Setup} out of range");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Countdown clamp: never exceeds ±13 ───
|
||||
|
||||
[Fact]
|
||||
public void Countdown_NeverExceedsThirteen()
|
||||
{
|
||||
var bars = MakeBars(500, seed: 7);
|
||||
var td = new TdSeq(4);
|
||||
foreach (var b in bars)
|
||||
{
|
||||
td.Update(b);
|
||||
Assert.True(td.Countdown >= -13 && td.Countdown <= 13,
|
||||
$"Countdown {td.Countdown} out of range");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Pre-warmup output is zero ───
|
||||
|
||||
[Fact]
|
||||
public void PreWarmup_OutputIsZero()
|
||||
{
|
||||
var td = new TdSeq(4);
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
double v = td.Update(new TBar(DateTime.UtcNow, 100 + i, 102 + i, 98 + i, 100 + i, 1000)).Value;
|
||||
Assert.Equal(0.0, v);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── NaN inputs: output remains finite ───
|
||||
|
||||
[Fact]
|
||||
public void NaN_OutputRemainsFinite()
|
||||
{
|
||||
var td = new TdSeq(4);
|
||||
var bars = MakeBars(20);
|
||||
foreach (var b in bars) { td.Update(b); }
|
||||
|
||||
// Insert NaN bar
|
||||
td.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 1000));
|
||||
Assert.True(double.IsFinite(td.Last.Value));
|
||||
}
|
||||
|
||||
// ─── Event-based matches streaming ───
|
||||
|
||||
[Fact]
|
||||
public void EventBased_MatchesStreaming()
|
||||
{
|
||||
var bars = MakeBars(300, seed: 55);
|
||||
|
||||
var streaming = new TdSeq(4);
|
||||
var streamResults = new double[bars.Length];
|
||||
for (int i = 0; i < bars.Length; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(bars[i]).Value;
|
||||
}
|
||||
|
||||
var barSource = new TBarSeries();
|
||||
var eventTd = new TdSeq(barSource, 4);
|
||||
var eventResults = new double[bars.Length];
|
||||
for (int i = 0; i < bars.Length; i++)
|
||||
{
|
||||
barSource.Add(bars[i]);
|
||||
eventResults[i] = eventTd.Last.Value;
|
||||
}
|
||||
|
||||
for (int i = 0; i < bars.Length; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], eventResults[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Different periods produce different results ───
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var bars = MakeBars(100);
|
||||
var td4 = new TdSeq(4);
|
||||
var td2 = new TdSeq(2);
|
||||
|
||||
bool anyDiff = false;
|
||||
foreach (var b in bars)
|
||||
{
|
||||
double v4 = td4.Update(b).Value;
|
||||
double v2 = td2.Update(b).Value;
|
||||
if (v4 != v2) { anyDiff = true; }
|
||||
}
|
||||
|
||||
Assert.True(anyDiff, "Period 4 and period 2 should produce different results on real data");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
// TD_SEQ: TD Sequential
|
||||
// Tom DeMark's exhaustion counting system — two-phase state machine.
|
||||
// Phase 1 (Setup): counts consecutive closes vs close[comparePeriod]; ±9 completes.
|
||||
// Phase 2 (Countdown): non-consecutive close vs high[2]/low[2]; ±13 completes.
|
||||
// All state is O(1) scalars — no circular buffers required.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// TD_SEQ: TD Sequential
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Tom DeMark's exhaustion counting system that identifies potential trend reversals
|
||||
/// through two phases:
|
||||
/// <list type="bullet">
|
||||
/// <item>Phase 1 — Setup (±1 to ±9): consecutive closes vs close[comparePeriod].
|
||||
/// Positive = sell setup, negative = buy setup. Completes at ±9.</item>
|
||||
/// <item>Phase 2 — Countdown (±1 to ±13): non-consecutive close vs high/low[2].
|
||||
/// Begins after a completed setup. Completes at ±13.</item>
|
||||
/// </list>
|
||||
/// All state maintained in O(1) scalar variables — no buffers needed beyond
|
||||
/// a small fixed history ring for close[comparePeriod], high[2], and low[2].
|
||||
/// <para>
|
||||
/// References:
|
||||
/// DeMark, T.R. (1994). The New Science of Technical Analysis. Wiley.
|
||||
/// PineScript reference: td_seq.pine
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class TdSeq : ITValuePublisher
|
||||
{
|
||||
private readonly int _comparePeriod;
|
||||
private readonly int _closeSize; // = comparePeriod + 1
|
||||
|
||||
// Close history ring: stores last (comparePeriod+1) values so we can read close[comparePeriod]
|
||||
private readonly double[] _closeHist;
|
||||
private readonly double[] _closeSnap;
|
||||
private int _closeIdx; // next write slot
|
||||
private int _closeCount; // how many slots filled (0.._closeSize)
|
||||
private int _closeIdxSnap;
|
||||
private int _closeCountSnap;
|
||||
|
||||
// High/Low history ring: stores last 3 values for high[2] / low[2]
|
||||
private readonly double[] _highHist;
|
||||
private readonly double[] _lowHist;
|
||||
private readonly double[] _highSnap;
|
||||
private readonly double[] _lowSnap;
|
||||
private int _hlIdx; // next write slot (mod 3)
|
||||
private int _hlCount; // how many slots filled (0..3)
|
||||
private int _hlIdxSnap;
|
||||
private int _hlCountSnap;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
int SetupCount,
|
||||
int CountdownCount,
|
||||
int CountdownDir,
|
||||
bool SetupComplete,
|
||||
double LastValidClose,
|
||||
double LastValidHigh,
|
||||
double LastValidLow);
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
private readonly TBarPublishedHandler _barHandler;
|
||||
|
||||
/// <summary>Display name of the indicator.</summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>Bars required before Phase 1 produces valid output.</summary>
|
||||
public int WarmupPeriod { get; }
|
||||
|
||||
/// <summary>True once enough close history exists to compare close[comparePeriod].</summary>
|
||||
public bool IsHot => _closeCount > _comparePeriod;
|
||||
|
||||
/// <summary>Current setup count (−9..+9). Positive = sell setup, negative = buy setup.</summary>
|
||||
public int Setup => _s.SetupCount;
|
||||
|
||||
/// <summary>Current countdown count (−13..+13). Non-zero only after a completed setup.</summary>
|
||||
public int Countdown => _s.CountdownCount;
|
||||
|
||||
/// <summary>Last published TValue. Value = countdown when active; setup otherwise.</summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <inheritdoc cref="ITValuePublisher.Pub"/>
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>Creates TD Sequential with the specified compare period.</summary>
|
||||
/// <param name="comparePeriod">Bars back for setup comparison (default 4, must be > 0)</param>
|
||||
public TdSeq(int comparePeriod = 4)
|
||||
{
|
||||
if (comparePeriod <= 0)
|
||||
{
|
||||
throw new ArgumentException("Compare period must be greater than 0", nameof(comparePeriod));
|
||||
}
|
||||
|
||||
_comparePeriod = comparePeriod;
|
||||
_closeSize = comparePeriod + 1;
|
||||
|
||||
_closeHist = new double[_closeSize];
|
||||
_closeSnap = new double[_closeSize];
|
||||
_highHist = new double[3];
|
||||
_lowHist = new double[3];
|
||||
_highSnap = new double[3];
|
||||
_lowSnap = new double[3];
|
||||
|
||||
Name = $"TdSeq({comparePeriod})";
|
||||
WarmupPeriod = comparePeriod + 1;
|
||||
|
||||
_barHandler = HandleBar;
|
||||
}
|
||||
|
||||
/// <summary>Creates TD Sequential subscribed to a bar publisher.</summary>
|
||||
public TdSeq(TBarSeries source, int comparePeriod = 4) : this(comparePeriod)
|
||||
{
|
||||
source.Pub += _barHandler;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void PubEvent(TValue value, bool isNew) =>
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
|
||||
|
||||
/// <summary>
|
||||
/// Processes a bar and returns the current indicator value.
|
||||
/// </summary>
|
||||
/// <param name="input">OHLCV bar (Close for setup, High/Low for countdown)</param>
|
||||
/// <param name="isNew">True to advance state; false to rewrite the current bar</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
// Sanitize inputs — substitute last-valid on non-finite
|
||||
double close = double.IsFinite(input.Close) ? input.Close : _s.LastValidClose;
|
||||
double high = double.IsFinite(input.High) ? input.High : _s.LastValidHigh;
|
||||
double low = double.IsFinite(input.Low) ? input.Low : _s.LastValidLow;
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
// Snapshot before mutation
|
||||
_ps = _s;
|
||||
Array.Copy(_closeHist, _closeSnap, _closeSize);
|
||||
Array.Copy(_highHist, _highSnap, 3);
|
||||
Array.Copy(_lowHist, _lowSnap, 3);
|
||||
_closeIdxSnap = _closeIdx;
|
||||
_closeCountSnap = _closeCount;
|
||||
_hlIdxSnap = _hlIdx;
|
||||
_hlCountSnap = _hlCount;
|
||||
|
||||
// Advance close ring
|
||||
_closeHist[_closeIdx] = close;
|
||||
_closeIdx = (_closeIdx + 1) % _closeSize;
|
||||
if (_closeCount < _closeSize) { _closeCount++; }
|
||||
|
||||
// Advance hi/lo ring
|
||||
_highHist[_hlIdx] = high;
|
||||
_lowHist[_hlIdx] = low;
|
||||
_hlIdx = (_hlIdx + 1) % 3;
|
||||
if (_hlCount < 3) { _hlCount++; }
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rollback rings to snapshot
|
||||
_s = _ps;
|
||||
Array.Copy(_closeSnap, _closeHist, _closeSize);
|
||||
Array.Copy(_highSnap, _highHist, 3);
|
||||
Array.Copy(_lowSnap, _lowHist, 3);
|
||||
_closeIdx = _closeIdxSnap;
|
||||
_closeCount = _closeCountSnap;
|
||||
_hlIdx = _hlIdxSnap;
|
||||
_hlCount = _hlCountSnap;
|
||||
|
||||
// Re-write newest slots with corrected values
|
||||
int newestClose = ((_closeIdx - 1) + _closeSize) % _closeSize;
|
||||
_closeHist[newestClose] = close;
|
||||
int newestHl = ((_hlIdx - 1) + 3) % 3;
|
||||
_highHist[newestHl] = high;
|
||||
_lowHist[newestHl] = low;
|
||||
}
|
||||
|
||||
// Track last-valid prices for NaN substitution
|
||||
if (double.IsFinite(input.Close)) { _s.LastValidClose = close; }
|
||||
if (double.IsFinite(input.High)) { _s.LastValidHigh = high; }
|
||||
if (double.IsFinite(input.Low)) { _s.LastValidLow = low; }
|
||||
|
||||
if (!IsHot)
|
||||
{
|
||||
Last = new TValue(input.Time, 0.0);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
// close[comparePeriod] = the oldest entry in the close ring:
|
||||
// after writing, _closeIdx points to the NEXT write slot.
|
||||
// That slot holds the oldest value (it is _comparePeriod bars ago).
|
||||
double prevClose = _closeHist[_closeIdx % _closeSize];
|
||||
|
||||
// --- Phase 1: Setup counting ---
|
||||
State s = _s;
|
||||
int newSetup;
|
||||
if (close < prevClose)
|
||||
{
|
||||
newSetup = s.SetupCount < 0 ? s.SetupCount - 1 : -1;
|
||||
}
|
||||
else if (close > prevClose)
|
||||
{
|
||||
newSetup = s.SetupCount > 0 ? s.SetupCount + 1 : 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
newSetup = 0;
|
||||
}
|
||||
|
||||
if (newSetup > 9) { newSetup = 9; }
|
||||
if (newSetup < -9) { newSetup = -9; }
|
||||
|
||||
// Detect completed setup (first time reaching ±9)
|
||||
if (Math.Abs(newSetup) == 9 && !s.SetupComplete)
|
||||
{
|
||||
s.SetupComplete = true;
|
||||
s.CountdownCount = 0;
|
||||
s.CountdownDir = newSetup > 0 ? 1 : -1;
|
||||
}
|
||||
|
||||
// Clear setupComplete if streak broke or reversed
|
||||
if (Math.Abs(newSetup) < Math.Abs(s.SetupCount) ||
|
||||
(newSetup > 0 && s.SetupCount < 0) ||
|
||||
(newSetup < 0 && s.SetupCount > 0))
|
||||
{
|
||||
s.SetupComplete = false;
|
||||
}
|
||||
|
||||
s.SetupCount = newSetup;
|
||||
|
||||
// --- Phase 2: Countdown (non-consecutive) ---
|
||||
if (s.CountdownDir != 0 && _hlCount >= 3)
|
||||
{
|
||||
// high[2] and low[2] = oldest entry in the 3-element hi/lo ring
|
||||
// After writing, _hlIdx points to the next write slot = oldest slot
|
||||
int oldestHl = _hlIdx % 3;
|
||||
double high2 = _highHist[oldestHl];
|
||||
double low2 = _lowHist[oldestHl];
|
||||
|
||||
if (s.CountdownDir == -1 && close < low2)
|
||||
{
|
||||
s.CountdownCount--;
|
||||
}
|
||||
else if (s.CountdownDir == 1 && close > high2)
|
||||
{
|
||||
s.CountdownCount++;
|
||||
}
|
||||
|
||||
if (Math.Abs(s.CountdownCount) >= 13)
|
||||
{
|
||||
s.CountdownCount = s.CountdownDir == 1 ? 13 : -13;
|
||||
s.CountdownDir = 0;
|
||||
}
|
||||
|
||||
// Opposite ±9 setup resets countdown
|
||||
if ((s.CountdownDir == 1 && newSetup == -9) ||
|
||||
(s.CountdownDir == -1 && newSetup == 9))
|
||||
{
|
||||
s.CountdownCount = 0;
|
||||
s.CountdownDir = newSetup > 0 ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
_s = s;
|
||||
|
||||
// Output: countdown value when active; setup value otherwise
|
||||
double result = (double)(_s.CountdownDir != 0 ? _s.CountdownCount : _s.SetupCount);
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>Resets all state and history to zero.</summary>
|
||||
public void Reset()
|
||||
{
|
||||
_s = default;
|
||||
_ps = default;
|
||||
Array.Clear(_closeHist);
|
||||
Array.Clear(_closeSnap);
|
||||
Array.Clear(_highHist);
|
||||
Array.Clear(_lowHist);
|
||||
Array.Clear(_highSnap);
|
||||
Array.Clear(_lowSnap);
|
||||
_closeIdx = 0;
|
||||
_closeCount = 0;
|
||||
_closeIdxSnap = 0;
|
||||
_closeCountSnap = 0;
|
||||
_hlIdx = 0;
|
||||
_hlCount = 0;
|
||||
_hlIdxSnap = 0;
|
||||
_hlCountSnap = 0;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates TD Sequential for an entire bar series.
|
||||
/// </summary>
|
||||
/// <param name="source">Source bar series</param>
|
||||
/// <param name="comparePeriod">Bars back for setup comparison (default 4)</param>
|
||||
/// <returns>TSeries containing the combined setup/countdown output per bar</returns>
|
||||
public static TSeries Calculate(TBarSeries source, int comparePeriod = 4)
|
||||
{
|
||||
var indicator = new TdSeq(comparePeriod);
|
||||
int len = source.Count;
|
||||
var results = new TSeries();
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
results.Add(indicator.Update(source[i], isNew: true));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user