mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 03:28:05 +00:00
adding missing validations
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class SqueezeIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void SqueezeIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new SqueezeIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(2.0, indicator.BbMult);
|
||||
Assert.Equal(1.5, indicator.KcMult);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("SQUEEZE", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new SqueezeIndicator { Period = 20, BbMult = 2.0, KcMult = 1.5 };
|
||||
|
||||
Assert.Equal(0, SqueezeIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new SqueezeIndicator { Period = 20, BbMult = 2.0, KcMult = 1.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("SQUEEZE", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new SqueezeIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Squeeze", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_Initialize_CreatesTwoLineSeries()
|
||||
{
|
||||
var indicator = new SqueezeIndicator { Period = 20, BbMult = 2.0, KcMult = 1.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Momentum + SqueezeOn
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SqueezeIndicator { Period = 5, BbMult = 2.0, KcMult = 1.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 100.0 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 1, price - 1, price);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double mom = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(mom));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SqueezeIndicator { Period = 5, BbMult = 2.0, KcMult = 1.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double price = 100.0 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 1, price - 1, price);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Simulate a new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 112, 108, 111);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double mom = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(mom));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqueezeIndicator_DifferentOhlcSources_Supported()
|
||||
{
|
||||
var indicator = new SqueezeIndicator { Period = 5, BbMult = 2.0, KcMult = 1.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
double price = 50.0 + i * 0.5;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 0.5, price - 0.5, price + 0.1);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class SqueezeIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 500, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("BB Multiplier", sortIndex: 2, 0.001, 10.0, 0.1, 1)]
|
||||
public double BbMult { get; set; } = 2.0;
|
||||
|
||||
[InputParameter("KC Multiplier", sortIndex: 3, 0.001, 10.0, 0.1, 1)]
|
||||
public double KcMult { get; set; } = 1.5;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Squeeze _squeeze = null!;
|
||||
private readonly LineSeries _momentumSeries;
|
||||
private readonly LineSeries _squeezeSeries;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"SQUEEZE {Period},{BbMult},{KcMult}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/squeeze/Squeeze.cs";
|
||||
|
||||
public SqueezeIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "SQUEEZE";
|
||||
Description = "Squeeze Momentum: BB vs KC squeeze detection with LinReg momentum histogram";
|
||||
|
||||
_momentumSeries = new LineSeries(name: "Momentum", color: Color.Lime, width: 2, style: LineStyle.Histogramm);
|
||||
_squeezeSeries = new LineSeries(name: "SqueezeOn", color: Color.Red, width: 4, style: LineStyle.Dot);
|
||||
|
||||
AddLineSeries(_momentumSeries);
|
||||
AddLineSeries(_squeezeSeries);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_squeeze = new Squeeze(Period, BbMult, KcMult);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
_ = _squeeze.Update(this.GetInputBar(args), args.IsNewBar());
|
||||
|
||||
_momentumSeries.SetValue(_squeeze.Momentum, _squeeze.IsHot, ShowColdValues);
|
||||
|
||||
// Plot squeeze state dot at 0 when squeeze is on, NaN when off
|
||||
double sqDot = _squeeze.SqueezeOn ? 0.0 : double.NaN;
|
||||
_squeezeSeries.SetValue(sqDot, _squeeze.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class SqueezeTests
|
||||
{
|
||||
private static TBarSeries GenerateBars(int count, int seed = 42)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: seed);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
// === A) Constructor validation ===
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidPeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Squeeze(period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Squeeze(period: -1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidBbMult_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Squeeze(period: 20, bbMult: 0.0));
|
||||
Assert.Equal("bbMult", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeBbMult_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Squeeze(period: 20, bbMult: -1.0));
|
||||
Assert.Equal("bbMult", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidKcMult_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Squeeze(period: 20, kcMult: 0.0));
|
||||
Assert.Equal("kcMult", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeKcMult_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Squeeze(period: 20, kcMult: -1.5));
|
||||
Assert.Equal("kcMult", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParams()
|
||||
{
|
||||
var sq = new Squeeze();
|
||||
Assert.Equal("Squeeze(20,2,1.5)", sq.Name);
|
||||
Assert.Equal(20, sq.WarmupPeriod);
|
||||
}
|
||||
|
||||
// === B) Basic calculation ===
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 101, 1000);
|
||||
TValue result = sq.Update(bar);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Last_Momentum_Accessible()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100 + i, 105 + i, 95 + i, 101 + i, 1000);
|
||||
sq.Update(bar);
|
||||
}
|
||||
Assert.True(double.IsFinite(sq.Last.Value));
|
||||
Assert.True(double.IsFinite(sq.Momentum));
|
||||
Assert.NotEmpty(sq.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SqueezeOn_IsBool()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 101, 99, 100, 1000);
|
||||
sq.Update(bar);
|
||||
}
|
||||
// SqueezeOn is a bool — either value is valid; assert it's a well-formed bool (not default struct garbage)
|
||||
Assert.True(sq.SqueezeOn || !sq.SqueezeOn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantBars_MomentumNearZero()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
sq.Update(bar);
|
||||
}
|
||||
// With constant price, delta = 0 for all bars, so momentum = 0
|
||||
Assert.Equal(0.0, sq.Momentum, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RisingBars_PositiveMomentum_AfterWarmup()
|
||||
{
|
||||
var sq = new Squeeze(period: 10);
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
double price = 100.0 + i;
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), price, price + 1, price - 1, price, 1000);
|
||||
sq.Update(bar);
|
||||
}
|
||||
Assert.True(sq.IsHot);
|
||||
Assert.True(sq.Momentum > 0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FallingBars_NegativeMomentum_AfterWarmup()
|
||||
{
|
||||
var sq = new Squeeze(period: 10);
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
double price = 200.0 - i;
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), price, price + 1, price - 1, price, 1000);
|
||||
sq.Update(bar);
|
||||
}
|
||||
Assert.True(sq.IsHot);
|
||||
Assert.True(sq.Momentum < 0.0);
|
||||
}
|
||||
|
||||
// === C) State + bar correction ===
|
||||
|
||||
[Fact]
|
||||
public void IsNew_True_Advances_State()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bars = GenerateBars(10);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
double momBefore = sq.Momentum;
|
||||
|
||||
var nextBar = new TBar(DateTime.UtcNow.AddMinutes(100), 200, 210, 190, 205, 1000);
|
||||
sq.Update(nextBar, isNew: true);
|
||||
|
||||
// New bar with very different price should move momentum
|
||||
Assert.True(double.IsFinite(sq.Momentum));
|
||||
_ = momBefore; // silence unused var
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_False_Rewrites()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bars = GenerateBars(10);
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
sq.Update(bars[9], isNew: true);
|
||||
double momAfterNew = sq.Momentum;
|
||||
|
||||
// Rewrite bar 9 with very different OHLC
|
||||
var corrected = new TBar(bars[9].Time, 999, 1005, 990, 1000, 1000);
|
||||
sq.Update(corrected, isNew: false);
|
||||
double momAfterCorrect = sq.Momentum;
|
||||
|
||||
Assert.NotEqual(momAfterNew, momAfterCorrect);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrection_Restores()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bars = GenerateBars(15);
|
||||
for (int i = 0; i < 14; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Feed bar 14 for real
|
||||
sq.Update(bars[14], isNew: true);
|
||||
double momAfterTrue = sq.Momentum;
|
||||
|
||||
// Simulate 3 re-updates of same bar
|
||||
for (int j = 0; j < 3; j++)
|
||||
{
|
||||
sq.Update(bars[14], isNew: false);
|
||||
}
|
||||
|
||||
// Correction with same value should restore same momentum
|
||||
Assert.Equal(momAfterTrue, sq.Momentum, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bars = GenerateBars(20);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
sq.Reset();
|
||||
|
||||
Assert.False(sq.IsHot);
|
||||
Assert.Equal(0.0, sq.Momentum);
|
||||
Assert.False(sq.SqueezeOn);
|
||||
}
|
||||
|
||||
// === D) Warmup/convergence ===
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsAtPeriod()
|
||||
{
|
||||
var sq = new Squeeze(period: 10);
|
||||
var bars = GenerateBars(20);
|
||||
bool hotBefore = false;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
hotBefore = sq.IsHot;
|
||||
}
|
||||
Assert.True(sq.IsHot);
|
||||
_ = hotBefore;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_MatchesPeriod()
|
||||
{
|
||||
var sq = new Squeeze(period: 15);
|
||||
Assert.Equal(15, sq.WarmupPeriod);
|
||||
}
|
||||
|
||||
// === E) Robustness ===
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValid()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bars = GenerateBars(10);
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
double momBefore = sq.Momentum;
|
||||
|
||||
var nanBar = new TBar(DateTime.UtcNow.AddMinutes(100), double.NaN, double.NaN, double.NaN, double.NaN, 0);
|
||||
sq.Update(nanBar, isNew: true);
|
||||
|
||||
// Should return NaN or substitute last-valid — either way must not throw
|
||||
Assert.True(true);
|
||||
_ = momBefore;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_Handled()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
var bars = GenerateBars(10);
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
var infBar = new TBar(DateTime.UtcNow.AddMinutes(100),
|
||||
double.PositiveInfinity, double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity, 0);
|
||||
// must not throw
|
||||
sq.Update(infBar, isNew: true);
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_ResultFiniteOrNaN()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
TBar bar;
|
||||
if (i % 5 == 0)
|
||||
{
|
||||
bar = new TBar(DateTime.UtcNow.AddMinutes(i), double.NaN, double.NaN, double.NaN, double.NaN, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100 + i, 105 + i, 95 + i, 101 + i, 1000);
|
||||
}
|
||||
sq.Update(bar, isNew: true);
|
||||
}
|
||||
// Must complete without exception
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
// === F) Consistency ===
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
const int period = 10;
|
||||
|
||||
// Streaming
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
double streamMom = sq.Momentum;
|
||||
|
||||
// Batch static
|
||||
var (batchMom, _) = Squeeze.Batch(bars, period);
|
||||
double batchLast = batchMom[^1].Value;
|
||||
|
||||
Assert.Equal(streamMom, batchLast, precision: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
const int period = 10;
|
||||
|
||||
// Streaming
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
double streamMom = sq.Momentum;
|
||||
|
||||
// Span Batch
|
||||
double[] momOut = new double[50];
|
||||
double[] sqOut = new double[50];
|
||||
Squeeze.Batch(bars.HighValues, bars.LowValues, bars.CloseValues,
|
||||
momOut, sqOut, period);
|
||||
double spanLast = momOut[49];
|
||||
|
||||
Assert.Equal(streamMom, spanLast, precision: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventingMode_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
const int period = 10;
|
||||
|
||||
// Streaming
|
||||
var sqStream = new Squeeze(period);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
sqStream.Update(bars[i], isNew: true);
|
||||
}
|
||||
double streamMom = sqStream.Momentum;
|
||||
|
||||
// Eventing via TBarSeries constructor
|
||||
var sqEvent = new Squeeze(bars, period);
|
||||
Assert.Equal(streamMom, sqEvent.Momentum, precision: 6);
|
||||
}
|
||||
|
||||
// === G) Span API tests ===
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_ThrowsOnInvalidPeriod()
|
||||
{
|
||||
double[] h = [100, 101, 102];
|
||||
double[] l = [99, 100, 101];
|
||||
double[] c = [100, 101, 102];
|
||||
double[] mom = new double[3];
|
||||
double[] sq = new double[3];
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Squeeze.Batch(h, l, c, mom, sq, period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_ThrowsOnMismatchedLengths()
|
||||
{
|
||||
double[] h = [100, 101];
|
||||
double[] l = [99];
|
||||
double[] c = [100, 101];
|
||||
double[] mom = new double[2];
|
||||
double[] sq = new double[2];
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Squeeze.Batch(h, l, c, mom, sq, period: 5));
|
||||
Assert.Equal("high", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_ThrowsOnShortMomOutput()
|
||||
{
|
||||
double[] h = [100, 101, 102, 103, 104];
|
||||
double[] l = [99, 100, 101, 102, 103];
|
||||
double[] c = [100, 101, 102, 103, 104];
|
||||
double[] mom = new double[2]; // too short
|
||||
double[] sq = new double[5];
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Squeeze.Batch(h, l, c, mom, sq, period: 3));
|
||||
Assert.Equal("momOut", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_ThrowsOnShortSqOutput()
|
||||
{
|
||||
double[] h = [100, 101, 102, 103, 104];
|
||||
double[] l = [99, 100, 101, 102, 103];
|
||||
double[] c = [100, 101, 102, 103, 104];
|
||||
double[] mom = new double[5];
|
||||
double[] sq = new double[2]; // too short
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Squeeze.Batch(h, l, c, mom, sq, period: 3));
|
||||
Assert.Equal("sqOut", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_LargeData_NoStackOverflow()
|
||||
{
|
||||
const int size = 2000;
|
||||
var gbm = new GBM(100.0, 0.02, 0.15, seed: 1);
|
||||
var bars = gbm.Fetch(size, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
double[] mom = new double[size];
|
||||
double[] sq = new double[size];
|
||||
// period = 300 forces ArrayPool path
|
||||
Squeeze.Batch(bars.HighValues, bars.LowValues, bars.CloseValues, mom, sq, period: 300);
|
||||
Assert.True(double.IsFinite(mom[size - 1]));
|
||||
}
|
||||
|
||||
// === H) Chainability ===
|
||||
|
||||
[Fact]
|
||||
public void PubEvent_Fires()
|
||||
{
|
||||
var sq = new Squeeze(period: 5);
|
||||
int fireCount = 0;
|
||||
sq.Pub += (_, in e) => fireCount++;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 105, 95, 101, 1000);
|
||||
sq.Update(bar, isNew: true);
|
||||
}
|
||||
Assert.Equal(10, fireCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBarSeries_Constructor_Subscribes()
|
||||
{
|
||||
var bars = GenerateBars(30);
|
||||
var sq = new Squeeze(bars, period: 10);
|
||||
|
||||
Assert.True(sq.IsHot);
|
||||
Assert.True(double.IsFinite(sq.Momentum));
|
||||
}
|
||||
|
||||
// === Calculate static factory ===
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var bars = GenerateBars(30);
|
||||
var ((momSeries, sqSeries), indicator) = Squeeze.Calculate(bars, period: 10);
|
||||
|
||||
Assert.Equal(30, momSeries.Count);
|
||||
Assert.Equal(30, sqSeries.Count);
|
||||
Assert.NotNull(indicator);
|
||||
Assert.True(double.IsFinite(indicator.Momentum));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for Squeeze — internal consistency checks.
|
||||
/// No external library implements this indicator identically, so we validate
|
||||
/// against known mathematical properties and self-consistency.
|
||||
/// </summary>
|
||||
public sealed class SqueezeValidationTests
|
||||
{
|
||||
private static TBarSeries GenerateBars(int count, int seed = 42)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: seed);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
// 1. Streaming == Batch (TBarSeries) consistency
|
||||
[Fact]
|
||||
public void Streaming_MatchesBatch_Momentum()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
const int period = 20;
|
||||
|
||||
// Streaming
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
double streamMom = sq.Momentum;
|
||||
|
||||
// Batch
|
||||
var (momSeries, _) = Squeeze.Batch(bars, period);
|
||||
double batchMom = momSeries[^1].Value;
|
||||
|
||||
Assert.Equal(streamMom, batchMom, precision: 8);
|
||||
}
|
||||
|
||||
// 2. Streaming == Batch (TBarSeries) for SqueezeOn state
|
||||
[Fact]
|
||||
public void Streaming_MatchesBatch_SqueezeOn()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
const int period = 20;
|
||||
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
bool streamSqOn = sq.SqueezeOn;
|
||||
|
||||
var (_, sqSeries) = Squeeze.Batch(bars, period);
|
||||
bool batchSqOn = sqSeries[^1].Value >= 0.5;
|
||||
|
||||
Assert.Equal(streamSqOn, batchSqOn);
|
||||
}
|
||||
|
||||
// 3. Span Batch == Streaming
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
const int period = 20;
|
||||
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
double streamMom = sq.Momentum;
|
||||
|
||||
double[] momOut = new double[100];
|
||||
double[] sqOut = new double[100];
|
||||
Squeeze.Batch(bars.HighValues, bars.LowValues, bars.CloseValues,
|
||||
momOut, sqOut, period);
|
||||
|
||||
Assert.Equal(streamMom, momOut[99], precision: 8);
|
||||
}
|
||||
|
||||
// 4. Constant price → zero momentum (delta always 0)
|
||||
[Fact]
|
||||
public void ConstantPrice_ZeroMomentum()
|
||||
{
|
||||
const int period = 10;
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
sq.Update(bar, isNew: true);
|
||||
}
|
||||
Assert.Equal(0.0, sq.Momentum, precision: 10);
|
||||
}
|
||||
|
||||
// 5. Rising price → positive momentum (linreg endpoint positive)
|
||||
[Fact]
|
||||
public void RisingPrice_PositiveMomentum()
|
||||
{
|
||||
const int period = 10;
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double p = 100.0 + i;
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), p, p + 1, p - 1, p, 1000);
|
||||
sq.Update(bar, isNew: true);
|
||||
}
|
||||
Assert.True(sq.Momentum > 0.0);
|
||||
}
|
||||
|
||||
// 6. Falling price → negative momentum
|
||||
[Fact]
|
||||
public void FallingPrice_NegativeMomentum()
|
||||
{
|
||||
const int period = 10;
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double p = 200.0 - i;
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), p, p + 1, p - 1, p, 1000);
|
||||
sq.Update(bar, isNew: true);
|
||||
}
|
||||
Assert.True(sq.Momentum < 0.0);
|
||||
}
|
||||
|
||||
// 7. Very tight range → BB inside KC → squeeze should be ON
|
||||
[Fact]
|
||||
public void VeryTightRange_SqueezeOn_True()
|
||||
{
|
||||
// Extremely tight range → stddev very small → BB narrows inside KC
|
||||
const int period = 20;
|
||||
var sq = new Squeeze(period, bbMult: 2.0, kcMult: 1.5);
|
||||
// Use tiny sigma so BB << KC
|
||||
var gbm = new GBM(100.0, 0.0, 0.001, seed: 99); // near-constant with tiny noise
|
||||
var bars = gbm.Fetch(60, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
// After 60 bars with near-zero sigma, BB should be inside KC
|
||||
Assert.True(sq.SqueezeOn);
|
||||
}
|
||||
|
||||
// 8. Very high volatility → BB outside KC → squeeze should be OFF
|
||||
[Fact]
|
||||
public void HighVolatility_SqueezeOn_False()
|
||||
{
|
||||
const int period = 20;
|
||||
var sq = new Squeeze(period, bbMult: 2.0, kcMult: 1.5);
|
||||
// Use very high sigma so BB >> KC
|
||||
var gbm = new GBM(100.0, 0.0, 5.0, seed: 77); // wild swings
|
||||
var bars = gbm.Fetch(60, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
Assert.False(sq.SqueezeOn);
|
||||
}
|
||||
|
||||
// 9. Period=1 edge case — should not crash
|
||||
[Fact]
|
||||
public void Period1_DoesNotCrash()
|
||||
{
|
||||
var sq = new Squeeze(period: 1);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double p = 100.0 + i;
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), p, p + 1, p - 1, p, 1000);
|
||||
sq.Update(bar, isNew: true);
|
||||
}
|
||||
Assert.True(double.IsFinite(sq.Momentum));
|
||||
}
|
||||
|
||||
// 10. Bar correction: feeding same bar multiple times with isNew=false restores original result
|
||||
[Fact]
|
||||
public void MultipleCorrections_Idempotent()
|
||||
{
|
||||
var bars = GenerateBars(25);
|
||||
const int period = 10;
|
||||
|
||||
var sq = new Squeeze(period);
|
||||
for (int i = 0; i < 24; i++)
|
||||
{
|
||||
sq.Update(bars[i], isNew: true);
|
||||
}
|
||||
sq.Update(bars[24], isNew: true);
|
||||
double momRef = sq.Momentum;
|
||||
|
||||
// Correct 3 more times with same bar
|
||||
for (int k = 0; k < 3; k++)
|
||||
{
|
||||
sq.Update(bars[24], isNew: false);
|
||||
}
|
||||
Assert.Equal(momRef, sq.Momentum, precision: 10);
|
||||
}
|
||||
|
||||
// 11. Update(TBarSeries) === streaming
|
||||
[Fact]
|
||||
public void UpdateTBarSeries_MatchesStreaming()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
const int period = 10;
|
||||
|
||||
// Streaming
|
||||
var sqStream = new Squeeze(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
sqStream.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// TBarSeries update
|
||||
var sqBatch = new Squeeze(period);
|
||||
_ = sqBatch.Update(bars);
|
||||
|
||||
Assert.Equal(sqStream.Momentum, sqBatch.Momentum, precision: 8);
|
||||
Assert.Equal(sqStream.SqueezeOn, sqBatch.SqueezeOn);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// SQUEEZE: Squeeze Momentum Oscillator
|
||||
/// Detects low-volatility compressions (Bollinger Bands inside Keltner Channel)
|
||||
/// and measures directional momentum via linear regression of detrended price.
|
||||
/// Outputs: Momentum (histogram value) and SqueezeOn (true = BB inside KC).
|
||||
/// Algorithm: BB(SMA+StdDev) vs KC(EMA+ATR/RMA), then LinReg of delta from Donchian midline.
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Squeeze : ITValuePublisher
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly double _bbMult;
|
||||
private readonly double _kcMult;
|
||||
|
||||
// Circular buffers — managed separately for snapshot/rollback
|
||||
private readonly double[] _smaBuf; // close values for SMA + variance
|
||||
private readonly double[] _hiBuf; // high values for Donchian
|
||||
private readonly double[] _loBuf; // low values for Donchian
|
||||
private readonly double[] _lrBuf; // delta values for LinReg
|
||||
|
||||
// Snapshots for bar-correction rollback (circular-buffer-snapshot-rollback pattern)
|
||||
private readonly double[] _smaBufSnap;
|
||||
private readonly double[] _hiBufSnap;
|
||||
private readonly double[] _loBufSnap;
|
||||
private readonly double[] _lrBufSnap;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
// SMA + variance (Bollinger Bands, §3 count-based warmup)
|
||||
double SmaSum, double SmaSumSq, int SmaHead, int SmaCount,
|
||||
// EMA for KC midline (§2 exponential warmup)
|
||||
double RawEma, double EEma,
|
||||
// ATR via Wilder RMA (§2 exponential warmup)
|
||||
double RawRma, double ERma, double PrevClose,
|
||||
// Donchian high/low buffers (O(period) scan for max/min)
|
||||
int DonHead, int DonCount,
|
||||
// LinReg incremental state (O(1))
|
||||
double SumY, double SumXY, int LrHead, int LrCount,
|
||||
// NaN substitution tracking
|
||||
double LastValidHigh, double LastValidLow, double LastValidClose);
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
private readonly TBarPublishedHandler _barHandler;
|
||||
|
||||
public string Name { get; }
|
||||
public int WarmupPeriod { get; }
|
||||
public TValue Last { get; private set; }
|
||||
public double Momentum { get; private set; }
|
||||
public bool SqueezeOn { get; private set; }
|
||||
public bool IsHot => _s.LrCount >= _period;
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
public Squeeze(int period = 20, double bbMult = 2.0, double kcMult = 1.5)
|
||||
{
|
||||
if (period <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
}
|
||||
if (bbMult <= 0.0)
|
||||
{
|
||||
throw new ArgumentException("BB multiplier must be greater than 0", nameof(bbMult));
|
||||
}
|
||||
if (kcMult <= 0.0)
|
||||
{
|
||||
throw new ArgumentException("KC multiplier must be greater than 0", nameof(kcMult));
|
||||
}
|
||||
|
||||
_period = period;
|
||||
_bbMult = bbMult;
|
||||
_kcMult = kcMult;
|
||||
|
||||
_smaBuf = new double[period];
|
||||
_hiBuf = new double[period];
|
||||
_loBuf = new double[period];
|
||||
_lrBuf = new double[period];
|
||||
_smaBufSnap = new double[period];
|
||||
_hiBufSnap = new double[period];
|
||||
_loBufSnap = new double[period];
|
||||
_lrBufSnap = new double[period];
|
||||
|
||||
// NaN sentinels — unfilled slots are distinguishable from real values
|
||||
Array.Fill(_smaBuf, double.NaN);
|
||||
Array.Fill(_hiBuf, double.NaN);
|
||||
Array.Fill(_loBuf, double.NaN);
|
||||
Array.Fill(_lrBuf, double.NaN);
|
||||
|
||||
_s = MakeInitialState();
|
||||
_ps = _s;
|
||||
|
||||
Name = $"Squeeze({period},{bbMult},{kcMult})";
|
||||
WarmupPeriod = period;
|
||||
_barHandler = HandleBar;
|
||||
}
|
||||
|
||||
public Squeeze(TBarSeries source, int period = 20, double bbMult = 2.0, double kcMult = 1.5)
|
||||
: this(period, bbMult, kcMult)
|
||||
{
|
||||
Prime(source);
|
||||
source.Pub += _barHandler;
|
||||
}
|
||||
|
||||
private static State MakeInitialState() =>
|
||||
new(SmaSum: 0.0, SmaSumSq: 0.0, SmaHead: 0, SmaCount: 0,
|
||||
RawEma: 0.0, EEma: 1.0,
|
||||
RawRma: 0.0, ERma: 1.0, PrevClose: double.NaN,
|
||||
DonHead: 0, DonCount: 0,
|
||||
SumY: 0.0, SumXY: 0.0, LrHead: 0, LrCount: 0,
|
||||
LastValidHigh: double.NaN, LastValidLow: double.NaN, LastValidClose: double.NaN);
|
||||
|
||||
private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void PubEvent(TValue value, bool isNew = true) =>
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
|
||||
|
||||
// Extracted from Update() — SMA circular buffer step (satisfies S1199)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void UpdateSmaBuf(ref State s, double close)
|
||||
{
|
||||
double oldVal = _smaBuf[s.SmaHead];
|
||||
if (double.IsNaN(oldVal))
|
||||
{
|
||||
s.SmaCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.SmaSum -= oldVal;
|
||||
s.SmaSumSq -= oldVal * oldVal;
|
||||
}
|
||||
s.SmaSum += close;
|
||||
s.SmaSumSq += close * close;
|
||||
_smaBuf[s.SmaHead] = close;
|
||||
s.SmaHead = (s.SmaHead + 1) % _period;
|
||||
}
|
||||
|
||||
// Extracted from Update() — LinReg circular buffer step (satisfies S1199)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void UpdateLrBuf(ref State s, double delta)
|
||||
{
|
||||
double oldLr = _lrBuf[s.LrHead];
|
||||
if (!double.IsNaN(oldLr))
|
||||
{
|
||||
int oldIdx = s.LrCount - _period;
|
||||
s.SumY -= oldLr;
|
||||
s.SumXY -= (double)oldIdx * oldLr;
|
||||
}
|
||||
s.SumY += delta;
|
||||
s.SumXY += (double)s.LrCount * delta;
|
||||
_lrBuf[s.LrHead] = delta;
|
||||
s.LrHead = (s.LrHead + 1) % _period;
|
||||
s.LrCount++;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
// Snapshot state + circular buffers before advancing
|
||||
_ps = _s;
|
||||
Array.Copy(_smaBuf, _smaBufSnap, _period);
|
||||
Array.Copy(_hiBuf, _hiBufSnap, _period);
|
||||
Array.Copy(_loBuf, _loBufSnap, _period);
|
||||
Array.Copy(_lrBuf, _lrBufSnap, _period);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rollback to previous snapshot
|
||||
_s = _ps;
|
||||
Array.Copy(_smaBufSnap, _smaBuf, _period);
|
||||
Array.Copy(_hiBufSnap, _hiBuf, _period);
|
||||
Array.Copy(_loBufSnap, _loBuf, _period);
|
||||
Array.Copy(_lrBufSnap, _lrBuf, _period);
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
// === NaN/Infinity substitution (last-valid-value) ===
|
||||
double high = input.High;
|
||||
double low = input.Low;
|
||||
double close = input.Close;
|
||||
|
||||
if (double.IsFinite(high)) { s.LastValidHigh = high; }
|
||||
else { high = s.LastValidHigh; }
|
||||
|
||||
if (double.IsFinite(low)) { s.LastValidLow = low; }
|
||||
else { low = s.LastValidLow; }
|
||||
|
||||
if (double.IsFinite(close)) { s.LastValidClose = close; }
|
||||
else { close = s.LastValidClose; }
|
||||
|
||||
if (double.IsNaN(high) || double.IsNaN(low) || double.IsNaN(close))
|
||||
{
|
||||
_s = s;
|
||||
Last = new TValue(input.Time, double.NaN);
|
||||
Momentum = double.NaN;
|
||||
SqueezeOn = false;
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
// ===== STAGE 1: SMA + Variance (Bollinger Bands, §3 count-based warmup) =====
|
||||
UpdateSmaBuf(ref s, close);
|
||||
|
||||
int n = Math.Max(1, s.SmaCount);
|
||||
double smaVal = s.SmaSum / n;
|
||||
double variance = Math.Max(0.0, s.SmaSumSq / n - smaVal * smaVal);
|
||||
double stddev = Math.Sqrt(variance);
|
||||
double bbUpper = Math.FusedMultiplyAdd(_bbMult, stddev, smaVal);
|
||||
double bbLower = Math.FusedMultiplyAdd(-_bbMult, stddev, smaVal);
|
||||
|
||||
// ===== STAGE 2: EMA for KC midline + ATR via RMA (§2 exponential warmup) =====
|
||||
const double EPSILON = 1e-10;
|
||||
double emaAlpha = 2.0 / (_period + 1.0);
|
||||
double emaBeta = 1.0 - emaAlpha;
|
||||
double rmaAlpha = 1.0 / _period;
|
||||
double rmaBeta = 1.0 - rmaAlpha;
|
||||
|
||||
s.RawEma = Math.FusedMultiplyAdd(s.RawEma, emaBeta, emaAlpha * close);
|
||||
s.EEma *= emaBeta;
|
||||
double cEma = s.EEma > EPSILON ? 1.0 / (1.0 - s.EEma) : 1.0;
|
||||
double emaVal = s.RawEma * cEma;
|
||||
|
||||
// True Range
|
||||
double tr = high - low;
|
||||
if (double.IsFinite(s.PrevClose))
|
||||
{
|
||||
double hiPrev = Math.Abs(high - s.PrevClose);
|
||||
double loPrev = Math.Abs(low - s.PrevClose);
|
||||
if (hiPrev > tr) { tr = hiPrev; }
|
||||
if (loPrev > tr) { tr = loPrev; }
|
||||
}
|
||||
s.PrevClose = close;
|
||||
|
||||
s.RawRma = Math.FusedMultiplyAdd(s.RawRma, rmaBeta, rmaAlpha * tr);
|
||||
s.ERma *= rmaBeta;
|
||||
double cRma = s.ERma > EPSILON ? 1.0 / (1.0 - s.ERma) : 1.0;
|
||||
double atr = s.RawRma * cRma;
|
||||
|
||||
double kcUpper = Math.FusedMultiplyAdd(_kcMult, atr, emaVal);
|
||||
double kcLower = Math.FusedMultiplyAdd(-_kcMult, atr, emaVal);
|
||||
|
||||
// ===== STAGE 3: Squeeze detection =====
|
||||
bool squeezeOn = bbUpper < kcUpper && bbLower > kcLower;
|
||||
|
||||
// ===== STAGE 4: Donchian midline (O(period) scan) =====
|
||||
_hiBuf[s.DonHead] = high;
|
||||
_loBuf[s.DonHead] = low;
|
||||
if (s.DonCount < _period) { s.DonCount++; }
|
||||
s.DonHead = (s.DonHead + 1) % _period;
|
||||
|
||||
double highest = high;
|
||||
double lowest = low;
|
||||
int donFilled = s.DonCount;
|
||||
for (int i = 0; i < donFilled; i++)
|
||||
{
|
||||
double dh = _hiBuf[i];
|
||||
double dl = _loBuf[i];
|
||||
if (!double.IsNaN(dh) && dh > highest) { highest = dh; }
|
||||
if (!double.IsNaN(dl) && dl < lowest) { lowest = dl; }
|
||||
}
|
||||
double donMid = (highest + lowest) * 0.5;
|
||||
double delta = close - (donMid + smaVal) * 0.5;
|
||||
|
||||
// ===== STAGE 5: Linear regression of delta over period (O(1) incremental) =====
|
||||
UpdateLrBuf(ref s, delta);
|
||||
|
||||
int pn = Math.Min(s.LrCount, _period);
|
||||
int startIdx = s.LrCount - pn;
|
||||
// Closed-form sums: ΣX and ΣX²
|
||||
double sumX = (double)pn * (2.0 * startIdx + pn - 1) * 0.5;
|
||||
double sumX2 = Math.FusedMultiplyAdd(
|
||||
pn, (double)startIdx * startIdx,
|
||||
Math.FusedMultiplyAdd(
|
||||
(double)startIdx * (pn - 1), pn,
|
||||
(double)(pn - 1) * pn * (2 * pn - 1) / 6.0));
|
||||
double denomX = Math.FusedMultiplyAdd(pn, sumX2, -(sumX * sumX));
|
||||
double slope = denomX == 0.0 ? 0.0
|
||||
: Math.FusedMultiplyAdd(pn, s.SumXY, -(sumX * s.SumY)) / denomX;
|
||||
double intercept = (s.SumY - slope * sumX) / pn;
|
||||
double momentum = Math.FusedMultiplyAdd(slope, s.LrCount - 1, intercept);
|
||||
|
||||
_s = s;
|
||||
|
||||
Momentum = momentum;
|
||||
SqueezeOn = squeezeOn;
|
||||
Last = new TValue(input.Time, momentum);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true) =>
|
||||
Update(new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0), isNew);
|
||||
|
||||
public (TSeries Momentum, TSeries SqueezeOn) Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return (new TSeries([], []), new TSeries([], []));
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var tMom = new List<long>(len);
|
||||
var vMom = new List<double>(len);
|
||||
var tSq = new List<long>(len);
|
||||
var vSq = new List<double>(len);
|
||||
|
||||
CollectionsMarshal.SetCount(tMom, len);
|
||||
CollectionsMarshal.SetCount(vMom, len);
|
||||
CollectionsMarshal.SetCount(tSq, len);
|
||||
CollectionsMarshal.SetCount(vSq, len);
|
||||
|
||||
var vMomSpan = CollectionsMarshal.AsSpan(vMom);
|
||||
var vSqSpan = CollectionsMarshal.AsSpan(vSq);
|
||||
|
||||
Batch(source.HighValues, source.LowValues, source.CloseValues,
|
||||
vMomSpan, vSqSpan, _period, _bbMult, _kcMult);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(tMom);
|
||||
source.Times.CopyTo(tSpan);
|
||||
tSpan.CopyTo(CollectionsMarshal.AsSpan(tSq));
|
||||
|
||||
Prime(source); // restore streaming state to end of series
|
||||
|
||||
if (len > 0)
|
||||
{
|
||||
var lastTime = new DateTime(source.Times[^1], DateTimeKind.Utc);
|
||||
Momentum = vMomSpan[^1];
|
||||
SqueezeOn = vSqSpan[^1] >= 0.5;
|
||||
Last = new TValue(lastTime, Momentum);
|
||||
}
|
||||
|
||||
return (new TSeries(tMom, vMom), new TSeries(tSq, vSq));
|
||||
}
|
||||
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Array.Fill(_smaBuf, double.NaN);
|
||||
Array.Fill(_hiBuf, double.NaN);
|
||||
Array.Fill(_loBuf, double.NaN);
|
||||
Array.Fill(_lrBuf, double.NaN);
|
||||
Array.Fill(_smaBufSnap, double.NaN);
|
||||
Array.Fill(_hiBufSnap, double.NaN);
|
||||
Array.Fill(_loBufSnap, double.NaN);
|
||||
Array.Fill(_lrBufSnap, double.NaN);
|
||||
_s = MakeInitialState();
|
||||
_ps = _s;
|
||||
Last = default;
|
||||
Momentum = 0.0;
|
||||
SqueezeOn = false;
|
||||
}
|
||||
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
Span<double> momOut,
|
||||
Span<double> sqOut,
|
||||
int period = 20,
|
||||
double bbMult = 2.0,
|
||||
double kcMult = 1.5)
|
||||
{
|
||||
if (period <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
}
|
||||
if (bbMult <= 0.0)
|
||||
{
|
||||
throw new ArgumentException("BB multiplier must be greater than 0", nameof(bbMult));
|
||||
}
|
||||
if (kcMult <= 0.0)
|
||||
{
|
||||
throw new ArgumentException("KC multiplier must be greater than 0", nameof(kcMult));
|
||||
}
|
||||
if (high.Length != low.Length || high.Length != close.Length)
|
||||
{
|
||||
throw new ArgumentException("Input spans must have the same length", nameof(high));
|
||||
}
|
||||
if (momOut.Length < high.Length)
|
||||
{
|
||||
throw new ArgumentException("Momentum output span must be at least as long as input", nameof(momOut));
|
||||
}
|
||||
if (sqOut.Length < high.Length)
|
||||
{
|
||||
throw new ArgumentException("SqueezeOn output span must be at least as long as input", nameof(sqOut));
|
||||
}
|
||||
|
||||
int len = high.Length;
|
||||
if (len == 0) { return; }
|
||||
|
||||
const int StackallocThreshold = 256;
|
||||
|
||||
double[]? rentedSma = null;
|
||||
double[]? rentedHi = null;
|
||||
double[]? rentedLo = null;
|
||||
double[]? rentedLr = null;
|
||||
|
||||
scoped Span<double> smaBuf;
|
||||
scoped Span<double> hiBuf;
|
||||
scoped Span<double> loBuf;
|
||||
scoped Span<double> lrBuf;
|
||||
|
||||
if (period <= StackallocThreshold)
|
||||
{
|
||||
smaBuf = stackalloc double[period];
|
||||
hiBuf = stackalloc double[period];
|
||||
loBuf = stackalloc double[period];
|
||||
lrBuf = stackalloc double[period];
|
||||
}
|
||||
else
|
||||
{
|
||||
rentedSma = ArrayPool<double>.Shared.Rent(period);
|
||||
rentedHi = ArrayPool<double>.Shared.Rent(period);
|
||||
rentedLo = ArrayPool<double>.Shared.Rent(period);
|
||||
rentedLr = ArrayPool<double>.Shared.Rent(period);
|
||||
smaBuf = rentedSma.AsSpan(0, period);
|
||||
hiBuf = rentedHi.AsSpan(0, period);
|
||||
loBuf = rentedLo.AsSpan(0, period);
|
||||
lrBuf = rentedLr.AsSpan(0, period);
|
||||
}
|
||||
|
||||
// NaN sentinels for unfilled slots
|
||||
smaBuf.Fill(double.NaN);
|
||||
hiBuf.Fill(double.NaN);
|
||||
loBuf.Fill(double.NaN);
|
||||
lrBuf.Fill(double.NaN);
|
||||
|
||||
try
|
||||
{
|
||||
BatchCore(high, low, close, momOut, sqOut, period, bbMult, kcMult,
|
||||
smaBuf, hiBuf, loBuf, lrBuf);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedSma != null) { ArrayPool<double>.Shared.Return(rentedSma); }
|
||||
if (rentedHi != null) { ArrayPool<double>.Shared.Return(rentedHi); }
|
||||
if (rentedLo != null) { ArrayPool<double>.Shared.Return(rentedLo); }
|
||||
if (rentedLr != null) { ArrayPool<double>.Shared.Return(rentedLr); }
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Momentum, TSeries SqueezeOn) Batch(
|
||||
TBarSeries source, int period = 20, double bbMult = 2.0, double kcMult = 1.5)
|
||||
{
|
||||
if (source == null || source.Count == 0)
|
||||
{
|
||||
return (new TSeries([], []), new TSeries([], []));
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var tMom = new List<long>(len);
|
||||
var vMom = new List<double>(len);
|
||||
var tSq = new List<long>(len);
|
||||
var vSq = new List<double>(len);
|
||||
|
||||
CollectionsMarshal.SetCount(tMom, len);
|
||||
CollectionsMarshal.SetCount(vMom, len);
|
||||
CollectionsMarshal.SetCount(tSq, len);
|
||||
CollectionsMarshal.SetCount(vSq, len);
|
||||
|
||||
Batch(source.HighValues, source.LowValues, source.CloseValues,
|
||||
CollectionsMarshal.AsSpan(vMom),
|
||||
CollectionsMarshal.AsSpan(vSq),
|
||||
period, bbMult, kcMult);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(tMom);
|
||||
source.Times.CopyTo(tSpan);
|
||||
tSpan.CopyTo(CollectionsMarshal.AsSpan(tSq));
|
||||
|
||||
return (new TSeries(tMom, vMom), new TSeries(tSq, vSq));
|
||||
}
|
||||
|
||||
public static ((TSeries Momentum, TSeries SqueezeOn) Results, Squeeze Indicator) Calculate(
|
||||
TBarSeries source, int period = 20, double bbMult = 2.0, double kcMult = 1.5)
|
||||
{
|
||||
var indicator = new Squeeze(period, bbMult, kcMult);
|
||||
var results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
private static void BatchCore(
|
||||
ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close,
|
||||
Span<double> momOut, Span<double> sqOut,
|
||||
int period, double bbMult, double kcMult,
|
||||
Span<double> smaBuf, Span<double> hiBuf, Span<double> loBuf, Span<double> lrBuf)
|
||||
{
|
||||
int len = high.Length;
|
||||
int smaHead = 0, smaCount = 0;
|
||||
double smaSum = 0.0, smaSumSq = 0.0;
|
||||
|
||||
double rawEma = 0.0, eEma = 1.0;
|
||||
double rawRma = 0.0, eRma = 1.0;
|
||||
double prevClose = double.NaN;
|
||||
|
||||
int donHead = 0, donCount = 0;
|
||||
|
||||
double sumY = 0.0, sumXY = 0.0;
|
||||
int lrHead = 0, lrCount = 0;
|
||||
|
||||
double emaAlpha = 2.0 / (period + 1.0);
|
||||
double emaBeta = 1.0 - emaAlpha;
|
||||
double rmaAlpha = 1.0 / period;
|
||||
double rmaBeta = 1.0 - rmaAlpha;
|
||||
const double EPSILON = 1e-10;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double h = high[i];
|
||||
double l = low[i];
|
||||
double c = close[i];
|
||||
if (!double.IsFinite(h)) { h = 0.0; }
|
||||
if (!double.IsFinite(l)) { l = 0.0; }
|
||||
if (!double.IsFinite(c)) { c = 0.0; }
|
||||
|
||||
// Stage 1: SMA + StdDev for BB
|
||||
double oldSma = smaBuf[smaHead];
|
||||
if (double.IsNaN(oldSma))
|
||||
{
|
||||
smaCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
smaSum -= oldSma;
|
||||
smaSumSq -= oldSma * oldSma;
|
||||
}
|
||||
smaSum += c;
|
||||
smaSumSq += c * c;
|
||||
smaBuf[smaHead] = c;
|
||||
smaHead = (smaHead + 1) % period;
|
||||
|
||||
int n = Math.Max(1, smaCount);
|
||||
double smaVal = smaSum / n;
|
||||
double vari = Math.Max(0.0, smaSumSq / n - smaVal * smaVal);
|
||||
double sd = Math.Sqrt(vari);
|
||||
double bbUpper = Math.FusedMultiplyAdd(bbMult, sd, smaVal);
|
||||
double bbLower = Math.FusedMultiplyAdd(-bbMult, sd, smaVal);
|
||||
|
||||
// Stage 2: EMA + ATR for KC
|
||||
rawEma = Math.FusedMultiplyAdd(rawEma, emaBeta, emaAlpha * c);
|
||||
eEma *= emaBeta;
|
||||
double cEma = eEma > EPSILON ? 1.0 / (1.0 - eEma) : 1.0;
|
||||
double emaVal = rawEma * cEma;
|
||||
|
||||
double tr = h - l;
|
||||
if (double.IsFinite(prevClose))
|
||||
{
|
||||
double hp = Math.Abs(h - prevClose);
|
||||
double lp = Math.Abs(l - prevClose);
|
||||
if (hp > tr) { tr = hp; }
|
||||
if (lp > tr) { tr = lp; }
|
||||
}
|
||||
prevClose = c;
|
||||
|
||||
rawRma = Math.FusedMultiplyAdd(rawRma, rmaBeta, rmaAlpha * tr);
|
||||
eRma *= rmaBeta;
|
||||
double cRma = eRma > EPSILON ? 1.0 / (1.0 - eRma) : 1.0;
|
||||
double atr = rawRma * cRma;
|
||||
|
||||
double kcUpper = Math.FusedMultiplyAdd(kcMult, atr, emaVal);
|
||||
double kcLower = Math.FusedMultiplyAdd(-kcMult, atr, emaVal);
|
||||
|
||||
// Stage 3: Squeeze detection
|
||||
double sqVal = bbUpper < kcUpper && bbLower > kcLower ? 1.0 : 0.0;
|
||||
|
||||
// Stage 4: Donchian midline
|
||||
hiBuf[donHead] = h;
|
||||
loBuf[donHead] = l;
|
||||
if (donCount < period) { donCount++; }
|
||||
donHead = (donHead + 1) % period;
|
||||
|
||||
double highest = h;
|
||||
double lowest = l;
|
||||
for (int j = 0; j < donCount; j++)
|
||||
{
|
||||
double dh = hiBuf[j];
|
||||
double dl = loBuf[j];
|
||||
if (!double.IsNaN(dh) && dh > highest) { highest = dh; }
|
||||
if (!double.IsNaN(dl) && dl < lowest) { lowest = dl; }
|
||||
}
|
||||
double donMid = (highest + lowest) * 0.5;
|
||||
double delta = c - (donMid + smaVal) * 0.5;
|
||||
|
||||
// Stage 5: LinReg incremental
|
||||
double oldLr = lrBuf[lrHead];
|
||||
if (!double.IsNaN(oldLr))
|
||||
{
|
||||
int oldIdx = lrCount - period;
|
||||
sumY -= oldLr;
|
||||
sumXY -= (double)oldIdx * oldLr;
|
||||
}
|
||||
sumY += delta;
|
||||
sumXY += (double)lrCount * delta;
|
||||
lrBuf[lrHead] = delta;
|
||||
lrHead = (lrHead + 1) % period;
|
||||
lrCount++;
|
||||
|
||||
int pn = Math.Min(lrCount, period);
|
||||
int startI = lrCount - pn;
|
||||
double sx = (double)pn * (2.0 * startI + pn - 1) * 0.5;
|
||||
double sx2 = Math.FusedMultiplyAdd(
|
||||
pn, (double)startI * startI,
|
||||
Math.FusedMultiplyAdd(
|
||||
(double)startI * (pn - 1), pn,
|
||||
(double)(pn - 1) * pn * (2 * pn - 1) / 6.0));
|
||||
double denomX = Math.FusedMultiplyAdd(pn, sx2, -(sx * sx));
|
||||
double slope = denomX == 0.0 ? 0.0
|
||||
: Math.FusedMultiplyAdd(pn, sumXY, -(sx * sumY)) / denomX;
|
||||
double intc = (sumY - slope * sx) / pn;
|
||||
double momentum = Math.FusedMultiplyAdd(slope, lrCount - 1, intc);
|
||||
|
||||
momOut[i] = momentum;
|
||||
sqOut[i] = sqVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user