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
+46 -13
View File
@@ -109,8 +109,6 @@ public sealed class Amfm : ITValuePublisher
_s = default;
_ps = default;
// Warmup: need 4 bars for envelope + 8 bars for SMA = 12;
// also need 'period' bars for SSF convergence
WarmupPeriod = Math.Max(12, period);
Name = $"Amfm({period})";
_barHandler = HandleBar;
@@ -201,9 +199,18 @@ public sealed class Amfm : ITValuePublisher
// Find max of the 4-element envelope buffer
double envel = _amEnvBuf[0];
if (_amEnvBuf[1] > envel) envel = _amEnvBuf[1];
if (_amEnvBuf[2] > envel) envel = _amEnvBuf[2];
if (_amEnvBuf[3] > envel) envel = _amEnvBuf[3];
if (_amEnvBuf[1] > envel)
{
envel = _amEnvBuf[1];
}
if (_amEnvBuf[2] > envel)
{
envel = _amEnvBuf[2];
}
if (_amEnvBuf[3] > envel)
{
envel = _amEnvBuf[3];
}
// Step 2: AM = SMA(envelope, 8)
int smaIdx = _s.SmaIdx;
@@ -224,8 +231,14 @@ public sealed class Amfm : ITValuePublisher
// ── FM Demodulator ───────────────────────────────────────────
// Step 1: Hard limiter (10x gain, clamp to ±1)
double hl = 10.0 * deriv;
if (hl > 1.0) hl = 1.0;
else if (hl < -1.0) hl = -1.0;
if (hl > 1.0)
{
hl = 1.0;
}
else if (hl < -1.0)
{
hl = -1.0;
}
// Step 2: Super Smoother (2-pole Butterworth IIR)
double fm;
@@ -301,7 +314,10 @@ public sealed class Amfm : ITValuePublisher
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (len == 0) return;
if (len == 0)
{
return;
}
// Super Smoother coefficients
double a1 = Math.Exp(-1.414 * Math.PI / period);
@@ -312,7 +328,9 @@ public sealed class Amfm : ITValuePublisher
// AM state
Span<double> envBuf = stackalloc double[4];
envBuf.Clear();
Span<double> smaBuf = stackalloc double[8];
smaBuf.Clear();
double smaSum = 0.0;
int envIdx = 0;
int smaIdx = 0;
@@ -332,9 +350,18 @@ public sealed class Amfm : ITValuePublisher
envIdx = (envIdx + 1) & 3;
double envel = envBuf[0];
if (envBuf[1] > envel) envel = envBuf[1];
if (envBuf[2] > envel) envel = envBuf[2];
if (envBuf[3] > envel) envel = envBuf[3];
if (envBuf[1] > envel)
{
envel = envBuf[1];
}
if (envBuf[2] > envel)
{
envel = envBuf[2];
}
if (envBuf[3] > envel)
{
envel = envBuf[3];
}
// AM: SMA(envelope, 8)
double oldSma = smaBuf[smaIdx];
@@ -346,8 +373,14 @@ public sealed class Amfm : ITValuePublisher
// FM: hard limiter
double hl = 10.0 * deriv;
if (hl > 1.0) hl = 1.0;
else if (hl < -1.0) hl = -1.0;
if (hl > 1.0)
{
hl = 1.0;
}
else if (hl < -1.0)
{
hl = -1.0;
}
// FM: Super Smoother
double fm;
+2 -2
View File
@@ -283,7 +283,7 @@ public class LpfIndicatorTests
public void LpfIndicator_SourceCodeLink_IsValid()
{
var indicator = new LpfIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink);
Assert.Contains("Lpf.Quantower.cs", indicator.SourceCodeLink);
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Lpf.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
}