feat: add TBF (Ehlers Truncated BandPass Filter) + fix all 64 warnings

TBF indicator:
- Sealed class with RingBuffer, stackalloc scratch, O(Length) per bar
- 7 core files: Tbf.cs, Tbf.Quantower.cs, Tbf.md, tbf.pine, 3 test files
- 67 tests (48 lib + 19 Quantower) all passing
- Full integration: sidebar, indexes, docs, Python bridge, exports

AMFM fix:
- Added envBuf.Clear()/smaBuf.Clear() after stackalloc in Batch
  (SkipLocalsInit garbage values caused 8.97e+65 blowup)

Warning fixes (64 → 0):
- Amfm.cs: S125 commented code removed, 11× IDE0011 braces
- Pta.cs: 11× IDE0011 braces on if/else/for/foreach
- Pta.Tests.cs: 14× IDE0011, S1481 unused var, S2699 assertion, 2× MA0074
- Lpf.Quantower.Tests.cs: 2× MA0074 StringComparison

Build: 0 warnings, 0 errors, 20,048 tests passing
This commit is contained in:
Miha Kralj
2026-03-18 19:10:48 -07:00
parent ef00330de3
commit b79b56dc65
21 changed files with 1992 additions and 33 deletions
+28 -3
View File
@@ -74,8 +74,10 @@ public sealed class Pta : AbstractBase
ArgumentOutOfRangeException.ThrowIfLessThan(longPeriod, 3, nameof(longPeriod));
ArgumentOutOfRangeException.ThrowIfLessThan(shortPeriod, 2, nameof(shortPeriod));
if (longPeriod <= shortPeriod)
{
throw new ArgumentOutOfRangeException(nameof(longPeriod),
$"longPeriod ({longPeriod}) must be greater than shortPeriod ({shortPeriod}).");
}
LongPeriod = longPeriod;
ShortPeriod = shortPeriod;
@@ -120,16 +122,22 @@ public sealed class Pta : AbstractBase
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double v in source)
{
Update(new TValue(DateTime.MinValue, v), isNew: true);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
double src = input.Value;
ref State s = ref _state;
@@ -188,7 +196,10 @@ public sealed class Pta : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
var resultValues = new double[source.Count];
Batch(source.Values, resultValues, LongPeriod, ShortPeriod);
@@ -196,7 +207,9 @@ public sealed class Pta : AbstractBase
var result = new TSeries();
var times = source.Times;
for (int i = 0; i < source.Count; i++)
{
result.Add(new TValue(times[i], resultValues[i]));
}
// Sync internal state
int len = source.Count;
@@ -204,7 +217,9 @@ public sealed class Pta : AbstractBase
{
var replay = new Pta(LongPeriod, ShortPeriod);
for (int i = 0; i < len; i++)
{
replay.Update(new TValue(times[i], source.Values[i]));
}
_state = replay._state;
}
_p_state = _state;
@@ -226,14 +241,21 @@ public sealed class Pta : AbstractBase
int longPeriod = 250, int shortPeriod = 40)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output spans must be of equal length.", nameof(output));
if (source.Length == 0) return;
}
if (source.Length == 0)
{
return;
}
ArgumentOutOfRangeException.ThrowIfLessThan(longPeriod, 3, nameof(longPeriod));
ArgumentOutOfRangeException.ThrowIfLessThan(shortPeriod, 2, nameof(shortPeriod));
if (longPeriod <= shortPeriod)
{
throw new ArgumentOutOfRangeException(nameof(longPeriod),
$"longPeriod ({longPeriod}) must be greater than shortPeriod ({shortPeriod}).");
}
// Precompute coefficients
ComputeHpCoefficients(longPeriod, out double c1L, out double c2L, out double c3L);
@@ -241,7 +263,10 @@ public sealed class Pta : AbstractBase
// Bar 0 and 1: output = 0 (not enough history for 2nd-order diff)
output[0] = 0.0;
if (source.Length < 2) return;
if (source.Length < 2)
{
return;
}
output[1] = 0.0;
double hp1 = 0, hp1_1 = 0;
+47 -11
View File
@@ -95,7 +95,10 @@ public class PtaTests
{
var pta = new Pta(50, 10);
var series = MakeSeries(100);
foreach (var bar in series) pta.Update(bar);
foreach (var bar in series)
{
pta.Update(bar);
}
double val1 = pta.Update(new TValue(DateTime.UtcNow, 105.0), isNew: true).Value;
double val2 = pta.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 110.0), isNew: true).Value;
Assert.NotEqual(val1, val2);
@@ -106,10 +109,13 @@ public class PtaTests
{
var pta = new Pta(50, 10);
var series = MakeSeries(100);
foreach (var bar in series) pta.Update(bar);
foreach (var bar in series)
{
pta.Update(bar);
}
double v1 = pta.Update(new TValue(DateTime.UtcNow, 105.0), isNew: true).Value;
double v2 = pta.Update(new TValue(DateTime.UtcNow, 108.0), isNew: false).Value;
_ = pta.Update(new TValue(DateTime.UtcNow, 108.0), isNew: false).Value;
double v3 = pta.Update(new TValue(DateTime.UtcNow, 105.0), isNew: false).Value;
Assert.Equal(v1, v3, 10);
}
@@ -119,7 +125,10 @@ public class PtaTests
{
var pta = new Pta(50, 10);
var series = MakeSeries(100);
foreach (var bar in series) pta.Update(bar);
foreach (var bar in series)
{
pta.Update(bar);
}
pta.Reset();
Assert.False(pta.IsHot);
Assert.Equal(0.0, pta.Update(new TValue(DateTime.UtcNow, 100.0)).Value);
@@ -163,7 +172,10 @@ public class PtaTests
{
var pta = new Pta(50, 10);
var series = MakeSeries(5000);
foreach (var bar in series) pta.Update(bar);
foreach (var bar in series)
{
pta.Update(bar);
}
Assert.True(double.IsFinite(pta.Last.Value));
}
@@ -192,7 +204,10 @@ public class PtaTests
// Mode 1: Streaming
var streaming = new Pta(lp, sp);
foreach (var bar in series) streaming.Update(bar);
foreach (var bar in series)
{
streaming.Update(bar);
}
// Mode 2: Batch TSeries
var batchResult = Pta.Batch(series, lp, sp);
@@ -228,19 +243,24 @@ public class PtaTests
var streaming = new Pta(lp, sp);
var streamResults = new double[series.Count];
for (int i = 0; i < series.Count; i++)
{
streamResults[i] = streaming.Update(series[i]).Value;
}
var spanResults = new double[series.Count];
Pta.Batch(series.Values, spanResults, lp, sp);
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(streamResults[i], spanResults[i], 10);
}
}
[Fact]
public void SpanBatch_EmptyInput_NoThrow()
{
Pta.Batch(ReadOnlySpan<double>.Empty, Span<double>.Empty, 50, 10);
var exception = Record.Exception(() => Pta.Batch(ReadOnlySpan<double>.Empty, Span<double>.Empty, 50, 10));
Assert.Null(exception);
}
[Fact]
@@ -261,7 +281,9 @@ public class PtaTests
var source = new TSeries();
var pta = new Pta(source, longPeriod: 50, shortPeriod: 10);
for (int i = 0; i < 100; i++)
{
source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i * 0.1));
}
Assert.True(double.IsFinite(pta.Last.Value));
}
@@ -274,7 +296,9 @@ public class PtaTests
{
var pta = new Pta(50, 10);
for (int i = 0; i < 300; i++)
{
pta.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
}
// Constant price → zero 2nd-order difference → both HP = 0 → PTA = 0
Assert.Equal(0.0, pta.Last.Value, 10);
@@ -286,7 +310,9 @@ public class PtaTests
// A perfectly linear trend has zero 2nd derivative → HP outputs approach 0
var pta = new Pta(50, 10);
for (int i = 0; i < 500; i++)
{
pta.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i * 0.5));
}
// Both HP filters output 0 for pure linear → PTA ≈ 0
Assert.True(Math.Abs(pta.Last.Value) < 1.0,
@@ -303,7 +329,10 @@ public class PtaTests
{
double price = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 100.0);
pta.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
if (i > 300) lastAbsMax = Math.Max(lastAbsMax, Math.Abs(pta.Last.Value));
if (i > 300)
{
lastAbsMax = Math.Max(lastAbsMax, Math.Abs(pta.Last.Value));
}
}
Assert.True(lastAbsMax > 0.1,
$"Expected significant output for in-band sine, got max={lastAbsMax}");
@@ -315,10 +344,14 @@ public class PtaTests
var pta = new Pta(50, 10);
// Uptrend
for (int i = 0; i < 200; i++)
{
pta.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i * 0.5));
}
// Transition to downtrend
for (int i = 0; i < 200; i++)
{
pta.Update(new TValue(DateTime.UtcNow.AddMinutes(200 + i), 200.0 - i * 0.5));
}
// After sustained downtrend, PTA should detect the reversal
// (the sign change may take some bars due to the bandpass filter)
@@ -343,8 +376,8 @@ public class PtaTests
public void Name_IncludesBothPeriods()
{
var pta = new Pta(300, 60);
Assert.Contains("300", pta.Name);
Assert.Contains("60", pta.Name);
Assert.Contains("300", pta.Name, StringComparison.Ordinal);
Assert.Contains("60", pta.Name, StringComparison.Ordinal);
}
[Fact]
@@ -361,7 +394,10 @@ public class PtaTests
{
var pta = new Pta(50, 10);
var values = new double[100];
for (int i = 0; i < 100; i++) values[i] = 100.0 + i * 0.1;
for (int i = 0; i < 100; i++)
{
values[i] = 100.0 + i * 0.1;
}
pta.Prime(values);
Assert.True(pta.IsHot);
}