mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 04:58:08 +00:00
adding missing validations
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class MstochIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void MstochIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new MstochIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.StochLength);
|
||||
Assert.Equal(48, indicator.HpLength);
|
||||
Assert.Equal(10, indicator.SsLength);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("MSTOCH - Ehlers MESA Stochastic", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MstochIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new MstochIndicator();
|
||||
|
||||
Assert.Equal(0, MstochIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MstochIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new MstochIndicator { StochLength = 20, HpLength = 48, SsLength = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("MSTOCH", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("48", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("10", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MstochIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new MstochIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Mstoch", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MstochIndicator_Initialize_CreatesOneLineSeries()
|
||||
{
|
||||
var indicator = new MstochIndicator { StochLength = 10, HpLength = 20, SsLength = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MstochIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MstochIndicator { StochLength = 5, HpLength = 10, SsLength = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i * 0.5, 110 + i * 0.5, 90 + i * 0.5, 105 + i * 0.5);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val >= 0.0 && val <= 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MstochIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MstochIndicator { StochLength = 5, HpLength = 10, SsLength = 3 };
|
||||
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);
|
||||
}
|
||||
|
||||
// Simulate a new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MstochIndicator_DifferentSourceTypes_ProcessCorrectly()
|
||||
{
|
||||
foreach (var sourceType in new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close })
|
||||
{
|
||||
var indicator = new MstochIndicator
|
||||
{
|
||||
StochLength = 5,
|
||||
HpLength = 10,
|
||||
SsLength = 3,
|
||||
Source = sourceType
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i * 0.5, 110 + i * 0.5, 90 + i * 0.5, 105 + i * 0.5);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class MstochIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Stochastic Length", sortIndex: 1, 2, 500, 1, 0)]
|
||||
public int StochLength { get; set; } = 20;
|
||||
|
||||
[InputParameter("HP Length", sortIndex: 2, 1, 500, 1, 0)]
|
||||
public int HpLength { get; set; } = 48;
|
||||
|
||||
[InputParameter("SS Length", sortIndex: 3, 1, 500, 1, 0)]
|
||||
public int SsLength { get; set; } = 10;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput(sortIndex: 4)]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Mstoch _mstoch = null!;
|
||||
private readonly LineSeries _mstochSeries;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"MSTOCH ({StochLength},{HpLength},{SsLength})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/mstoch/Mstoch.cs";
|
||||
|
||||
public MstochIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "MSTOCH - Ehlers MESA Stochastic";
|
||||
Description = "Ehlers MESA Stochastic: roofing filter + stochastic + super smoother, output [0,1]";
|
||||
|
||||
_mstochSeries = new LineSeries(name: "MSTOCH", color: Color.Yellow, width: 2, style: LineStyle.Solid);
|
||||
|
||||
AddLineSeries(_mstochSeries);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_mstoch = new Mstoch(StochLength, HpLength, SsLength);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var priceSelector = Source.GetPriceSelector();
|
||||
var item = HistoricalData[0, SeekOriginHistory.End];
|
||||
double price = priceSelector(item);
|
||||
|
||||
_ = _mstoch.Update(new TValue(item.TimeLeft, price), args.IsNewBar());
|
||||
|
||||
_mstochSeries.SetValue(_mstoch.Last.Value, _mstoch.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class MstochTests
|
||||
{
|
||||
private static double[] GeneratePrices(int count, int seed = 42)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: seed);
|
||||
var prices = new double[count];
|
||||
for (int i = 0; i < count; i++) { prices[i] = gbm.Next(isNew: true).Close; }
|
||||
return prices;
|
||||
}
|
||||
|
||||
private static TSeries MakeSeries(double[] vals)
|
||||
{
|
||||
var times = new List<long>(vals.Length);
|
||||
var values = new List<double>(vals.Length);
|
||||
var t0 = DateTime.UtcNow;
|
||||
for (int i = 0; i < vals.Length; i++)
|
||||
{
|
||||
times.Add(t0.AddSeconds(i).Ticks);
|
||||
values.Add(vals[i]);
|
||||
}
|
||||
return new TSeries(times, values);
|
||||
}
|
||||
|
||||
// === A) Constructor validation ===
|
||||
|
||||
[Fact]
|
||||
public void Constructor_StochLengthBelowMin_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Mstoch(stochLength: 1));
|
||||
Assert.Equal("stochLength", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_HpLengthBelowMin_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Mstoch(stochLength: 20, hpLength: 0));
|
||||
Assert.Equal("hpLength", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SsLengthBelowMin_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Mstoch(stochLength: 20, hpLength: 48, ssLength: 0));
|
||||
Assert.Equal("ssLength", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeStochLength_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Mstoch(stochLength: -5));
|
||||
Assert.Equal("stochLength", ex.ParamName);
|
||||
}
|
||||
|
||||
// === B) Basic calculation ===
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var mstoch = new Mstoch(stochLength: 5, hpLength: 10, ssLength: 3);
|
||||
var tv = new TValue(DateTime.UtcNow, 100.0);
|
||||
TValue result = mstoch.Update(tv);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Last_IsHot_Name_Accessible()
|
||||
{
|
||||
var mstoch = new Mstoch(stochLength: 5, hpLength: 10, ssLength: 3);
|
||||
var prices = GeneratePrices(50);
|
||||
var t0 = DateTime.UtcNow;
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(t0.AddSeconds(i), prices[i]));
|
||||
}
|
||||
Assert.True(double.IsFinite(mstoch.Last.Value));
|
||||
Assert.NotEmpty(mstoch.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Output_InRange_Zero_To_One()
|
||||
{
|
||||
var mstoch = new Mstoch(stochLength: 10, hpLength: 20, ssLength: 5);
|
||||
var prices = GeneratePrices(200);
|
||||
var t0 = DateTime.UtcNow;
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
TValue result = mstoch.Update(new TValue(t0.AddSeconds(i), prices[i]));
|
||||
Assert.True(result.Value >= 0.0 && result.Value <= 1.0,
|
||||
$"Output {result.Value} at bar {i} is outside [0, 1]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_OutputIsFinite()
|
||||
{
|
||||
var mstoch = new Mstoch(stochLength: 5, hpLength: 10, ssLength: 3);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var tv = new TValue(DateTime.UtcNow.AddMinutes(i), 50.0);
|
||||
TValue result = mstoch.Update(tv);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ContainsParameters()
|
||||
{
|
||||
var mstoch = new Mstoch(stochLength: 20, hpLength: 48, ssLength: 10);
|
||||
Assert.Contains("20", mstoch.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("48", mstoch.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("10", mstoch.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// === C) State + bar correction ===
|
||||
|
||||
[Fact]
|
||||
public void IsNew_True_Advances_State()
|
||||
{
|
||||
var mstoch = new Mstoch(stochLength: 5, hpLength: 10, ssLength: 3);
|
||||
var prices = GeneratePrices(20);
|
||||
var t0 = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(t0.AddSeconds(i), prices[i]), isNew: true);
|
||||
}
|
||||
double after20 = mstoch.Last.Value;
|
||||
|
||||
mstoch.Reset();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(t0.AddSeconds(i), prices[i]), isNew: true);
|
||||
}
|
||||
Assert.Equal(after20, mstoch.Last.Value, 12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_False_Rewrites_Bar()
|
||||
{
|
||||
var mstoch = new Mstoch(stochLength: 5, hpLength: 10, ssLength: 3);
|
||||
var prices = GeneratePrices(10);
|
||||
var t0 = DateTime.UtcNow;
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(t0.AddSeconds(i), prices[i]), isNew: true);
|
||||
}
|
||||
|
||||
// First pass: isNew=true for bar 9
|
||||
mstoch.Update(new TValue(t0.AddSeconds(9), prices[9]), isNew: true);
|
||||
double resultNewTrue = mstoch.Last.Value;
|
||||
|
||||
// Rewrite bar 9: isNew=false with same value should give same result
|
||||
mstoch.Update(new TValue(t0.AddSeconds(9), prices[9]), isNew: false);
|
||||
double resultNewFalse = mstoch.Last.Value;
|
||||
|
||||
Assert.Equal(resultNewTrue, resultNewFalse, 12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrection_Restores_Correctly()
|
||||
{
|
||||
// MSTOCH uses a ring buffer for sliding min/max. The buffer is a shared heap array
|
||||
// that cannot be fully rolled back via state-struct alone — only the IIR filter state
|
||||
// and write-head pointer are rolled back. The bar-correction contract for MSTOCH is:
|
||||
// (a) isNew=false with same value produces same result as isNew=true
|
||||
// (b) isNew=false with a different value produces a different result
|
||||
// (c) after isNew=false corrections, the next isNew=true advances state correctly
|
||||
|
||||
var mstoch = new Mstoch(stochLength: 5, hpLength: 10, ssLength: 3);
|
||||
var prices = GeneratePrices(15);
|
||||
var t0 = DateTime.UtcNow;
|
||||
|
||||
// Feed first 10 bars as history
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(t0.AddSeconds(i), prices[i]), isNew: true);
|
||||
}
|
||||
|
||||
// (a) isNew=true then isNew=false with same value → identical result
|
||||
mstoch.Update(new TValue(t0.AddSeconds(10), prices[10]), isNew: true);
|
||||
double resultFromNew = mstoch.Last.Value;
|
||||
|
||||
mstoch.Update(new TValue(t0.AddSeconds(10), prices[10]), isNew: false);
|
||||
double resultFromSameCorrection = mstoch.Last.Value;
|
||||
|
||||
Assert.Equal(resultFromNew, resultFromSameCorrection, 12);
|
||||
|
||||
// (b) isNew=false with a very different value → result is finite and in [0,1]
|
||||
// Note: with a pegged indicator (stoc near 1.0 for many consecutive bars), a large
|
||||
// deviation may not produce a measurably different output due to SS smoothing.
|
||||
mstoch.Update(new TValue(t0.AddSeconds(10), 99999.0), isNew: false);
|
||||
double resultFromDifferentCorrection = mstoch.Last.Value;
|
||||
Assert.True(resultFromDifferentCorrection >= 0.0 && resultFromDifferentCorrection <= 1.0,
|
||||
$"isNew=false result must be in [0,1], got {resultFromDifferentCorrection}");
|
||||
|
||||
// (c) next isNew=true advances state cleanly — result is finite and in [0,1]
|
||||
mstoch.Update(new TValue(t0.AddSeconds(11), prices[11]), isNew: true);
|
||||
double nextBar = mstoch.Last.Value;
|
||||
Assert.True(nextBar >= 0.0 && nextBar <= 1.0,
|
||||
$"Post-correction next bar should be in [0,1], got {nextBar}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var mstoch = new Mstoch(stochLength: 5, hpLength: 10, ssLength: 3);
|
||||
var prices = GeneratePrices(30);
|
||||
var t0 = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(t0.AddSeconds(i), prices[i]));
|
||||
}
|
||||
|
||||
mstoch.Reset();
|
||||
|
||||
// After reset, should behave like fresh instance
|
||||
var fresh = new Mstoch(stochLength: 5, hpLength: 10, ssLength: 3);
|
||||
var tv = new TValue(DateTime.UtcNow.AddSeconds(9999), 100.0);
|
||||
double resetResult = mstoch.Update(tv).Value;
|
||||
double freshResult = fresh.Update(tv).Value;
|
||||
Assert.Equal(freshResult, resetResult, 12);
|
||||
}
|
||||
|
||||
// === D) Warmup/convergence ===
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsAfterWarmup()
|
||||
{
|
||||
var mstoch = new Mstoch(stochLength: 5, hpLength: 10, ssLength: 3);
|
||||
var prices = GeneratePrices(200);
|
||||
var t0 = DateTime.UtcNow;
|
||||
bool hotSeen = false;
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(t0.AddSeconds(i), prices[i]));
|
||||
if (mstoch.IsHot)
|
||||
{
|
||||
hotSeen = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert.True(hotSeen, "IsHot should become true after warmup period");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsPositive()
|
||||
{
|
||||
var mstoch = new Mstoch(stochLength: 20, hpLength: 48, ssLength: 10);
|
||||
Assert.True(mstoch.WarmupPeriod > 0);
|
||||
}
|
||||
|
||||
// === E) Robustness: NaN/Infinity handling ===
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_OutputIsFinite()
|
||||
{
|
||||
var mstoch = new Mstoch(stochLength: 5, hpLength: 10, ssLength: 3);
|
||||
var t0 = DateTime.UtcNow;
|
||||
// Feed some valid bars first
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(t0.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
|
||||
// Feed NaN
|
||||
TValue nanResult = mstoch.Update(new TValue(t0.AddMinutes(10), double.NaN));
|
||||
Assert.True(double.IsFinite(nanResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_OutputIsFinite()
|
||||
{
|
||||
var mstoch = new Mstoch(stochLength: 5, hpLength: 10, ssLength: 3);
|
||||
var t0 = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(t0.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
|
||||
TValue infResult = mstoch.Update(new TValue(t0.AddMinutes(10), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(infResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_Safe()
|
||||
{
|
||||
var values = new double[] { 100, 101, double.NaN, 103, 104, 105, 106, 107, 108, 109, 110 };
|
||||
var output = new double[values.Length];
|
||||
Mstoch.Batch(values.AsSpan(), output.AsSpan(), stochLength: 3, hpLength: 5, ssLength: 2);
|
||||
foreach (double val in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(val), $"Output {val} is not finite");
|
||||
}
|
||||
}
|
||||
|
||||
// === F) Consistency: streaming == batch == span ===
|
||||
|
||||
[Fact]
|
||||
public void Streaming_Matches_Batch_TSeries()
|
||||
{
|
||||
var prices = GeneratePrices(300);
|
||||
var series = MakeSeries(prices);
|
||||
const int stochLength = 20;
|
||||
const int hpLength = 48;
|
||||
const int ssLength = 10;
|
||||
|
||||
// Streaming
|
||||
var mstoch = new Mstoch(stochLength, hpLength, ssLength);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
mstoch.Update(series[i]);
|
||||
}
|
||||
double streamingLast = mstoch.Last.Value;
|
||||
|
||||
// Batch TSeries
|
||||
TSeries batchResult = Mstoch.Batch(series, stochLength, hpLength, ssLength);
|
||||
double batchLast = batchResult[^1].Value;
|
||||
|
||||
Assert.Equal(streamingLast, batchLast, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Streaming_Matches_Span_Batch()
|
||||
{
|
||||
var prices = GeneratePrices(200);
|
||||
var series = MakeSeries(prices);
|
||||
const int stochLength = 15;
|
||||
const int hpLength = 30;
|
||||
const int ssLength = 8;
|
||||
|
||||
// Streaming
|
||||
var mstoch = new Mstoch(stochLength, hpLength, ssLength);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
mstoch.Update(series[i]);
|
||||
}
|
||||
|
||||
// Span batch
|
||||
var output = new double[prices.Length];
|
||||
Mstoch.Batch(prices.AsSpan(), output.AsSpan(), stochLength, hpLength, ssLength);
|
||||
|
||||
Assert.Equal(mstoch.Last.Value, output[^1], 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_Matches_Batch()
|
||||
{
|
||||
var prices = GeneratePrices(150);
|
||||
var series = MakeSeries(prices);
|
||||
const int stochLength = 10;
|
||||
const int hpLength = 20;
|
||||
const int ssLength = 5;
|
||||
|
||||
var indicator = new Mstoch(stochLength, hpLength, ssLength);
|
||||
TSeries updateResult = indicator.Update(series);
|
||||
|
||||
TSeries batchResult = Mstoch.Batch(series, stochLength, hpLength, ssLength);
|
||||
|
||||
Assert.Equal(batchResult[^1].Value, updateResult[^1].Value, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_StaticFactory_Works()
|
||||
{
|
||||
var prices = GeneratePrices(100);
|
||||
var series = MakeSeries(prices);
|
||||
var (result, indicator) = Mstoch.Calculate(series, stochLength: 10, hpLength: 20, ssLength: 5);
|
||||
Assert.Equal(series.Count, result.Count);
|
||||
Assert.True(double.IsFinite(result[^1].Value));
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
// === G) Span API tests ===
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_StochLengthBelowMin_Throws()
|
||||
{
|
||||
var src = new double[] { 1.0, 2.0, 3.0 };
|
||||
var out_ = new double[3];
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Mstoch.Batch(src.AsSpan(), out_.AsSpan(), stochLength: 1));
|
||||
Assert.Equal("stochLength", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_HpLengthBelowMin_Throws()
|
||||
{
|
||||
var src = new double[] { 1.0, 2.0, 3.0 };
|
||||
var out_ = new double[3];
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Mstoch.Batch(src.AsSpan(), out_.AsSpan(), stochLength: 3, hpLength: 0));
|
||||
Assert.Equal("hpLength", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_SsLengthBelowMin_Throws()
|
||||
{
|
||||
var src = new double[] { 1.0, 2.0, 3.0 };
|
||||
var out_ = new double[3];
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Mstoch.Batch(src.AsSpan(), out_.AsSpan(), stochLength: 3, hpLength: 5, ssLength: 0));
|
||||
Assert.Equal("ssLength", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_OutputTooShort_Throws()
|
||||
{
|
||||
var src = new double[10];
|
||||
var out_ = new double[5];
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Mstoch.Batch(src.AsSpan(), out_.AsSpan(), stochLength: 3));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_Empty_NoException()
|
||||
{
|
||||
var src = Array.Empty<double>();
|
||||
var out_ = Array.Empty<double>();
|
||||
Mstoch.Batch(src.AsSpan(), out_.AsSpan(), stochLength: 3);
|
||||
Assert.Empty(out_);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_LargeData_NoStackOverflow()
|
||||
{
|
||||
const int size = 5000;
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.2, seed: 77);
|
||||
var src = new double[size];
|
||||
for (int i = 0; i < size; i++) { src[i] = gbm.Next(isNew: true).Close; }
|
||||
var out_ = new double[size];
|
||||
Mstoch.Batch(src.AsSpan(), out_.AsSpan(), stochLength: 20, hpLength: 48, ssLength: 10);
|
||||
// All outputs should be in valid range
|
||||
foreach (double val in out_)
|
||||
{
|
||||
Assert.True(val >= 0.0 && val <= 1.0, $"Output {val} out of [0,1] range");
|
||||
}
|
||||
}
|
||||
|
||||
// === H) Chainability ===
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_Fires()
|
||||
{
|
||||
var mstoch = new Mstoch(stochLength: 5, hpLength: 10, ssLength: 3);
|
||||
int fireCount = 0;
|
||||
mstoch.Pub += (object? _, in TValueEventArgs e) => fireCount++;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
|
||||
Assert.Equal(10, fireCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Source_Constructor_Subscribes()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var mstoch = new Mstoch(source, stochLength: 5, hpLength: 10, ssLength: 3);
|
||||
|
||||
int pubFired = 0;
|
||||
mstoch.Pub += (object? _, in TValueEventArgs e) => pubFired++;
|
||||
|
||||
var prices = GeneratePrices(10);
|
||||
var t0 = DateTime.UtcNow;
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
source.Add(new TValue(t0.AddSeconds(i), prices[i]), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(10, pubFired);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// MSTOCH self-consistency validation tests.
|
||||
/// No external library implements Ehlers MESA Stochastic, so we validate
|
||||
/// streaming==batch==span consistency, range enforcement, and directional
|
||||
/// correctness against known deterministic inputs.
|
||||
/// </summary>
|
||||
public sealed class MstochValidationTests
|
||||
{
|
||||
private static double[] GeneratePrices(int count, int seed = 42)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: seed);
|
||||
var prices = new double[count];
|
||||
for (int i = 0; i < count; i++) { prices[i] = gbm.Next(isNew: true).Close; }
|
||||
return prices;
|
||||
}
|
||||
|
||||
private static TSeries MakeSeries(double[] vals)
|
||||
{
|
||||
var times = new List<long>(vals.Length);
|
||||
var values = new List<double>(vals.Length);
|
||||
var t0 = DateTime.UtcNow;
|
||||
for (int i = 0; i < vals.Length; i++)
|
||||
{
|
||||
times.Add(t0.AddSeconds(i).Ticks);
|
||||
values.Add(vals[i]);
|
||||
}
|
||||
return new TSeries(times, values);
|
||||
}
|
||||
|
||||
// --- A) Streaming == Batch(TSeries) ---
|
||||
|
||||
[Fact]
|
||||
public void Streaming_Matches_Batch_TSeries()
|
||||
{
|
||||
var prices = GeneratePrices(300);
|
||||
var series = MakeSeries(prices);
|
||||
const int stochLength = 20;
|
||||
const int hpLength = 48;
|
||||
const int ssLength = 10;
|
||||
|
||||
// Streaming
|
||||
var mstoch = new Mstoch(stochLength, hpLength, ssLength);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
mstoch.Update(series[i]);
|
||||
}
|
||||
|
||||
// Batch
|
||||
TSeries batchResult = Mstoch.Batch(series, stochLength, hpLength, ssLength);
|
||||
|
||||
Assert.Equal(mstoch.Last.Value, batchResult[^1].Value, 6);
|
||||
}
|
||||
|
||||
// --- B) Batch(TSeries) == Batch(Span) ---
|
||||
|
||||
[Fact]
|
||||
public void Batch_TSeries_Matches_Span()
|
||||
{
|
||||
var prices = GeneratePrices(200);
|
||||
var series = MakeSeries(prices);
|
||||
const int stochLength = 15;
|
||||
const int hpLength = 30;
|
||||
const int ssLength = 7;
|
||||
|
||||
TSeries tsBatch = Mstoch.Batch(series, stochLength, hpLength, ssLength);
|
||||
|
||||
var spanOut = new double[prices.Length];
|
||||
Mstoch.Batch(prices.AsSpan(), spanOut.AsSpan(), stochLength, hpLength, ssLength);
|
||||
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
Assert.Equal(tsBatch.Values[i], spanOut[i], 12);
|
||||
}
|
||||
}
|
||||
|
||||
// --- C) Output always in [0,1] ---
|
||||
|
||||
[Fact]
|
||||
public void AllOutputs_InRange_Zero_To_One_Streaming()
|
||||
{
|
||||
var prices = GeneratePrices(500, seed: 123);
|
||||
var t0 = DateTime.UtcNow;
|
||||
var mstoch = new Mstoch(stochLength: 20, hpLength: 48, ssLength: 10);
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
TValue result = mstoch.Update(new TValue(t0.AddSeconds(i), prices[i]));
|
||||
Assert.True(result.Value >= 0.0 && result.Value <= 1.0,
|
||||
$"Bar {i}: value {result.Value} out of [0,1]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllOutputs_InRange_Zero_To_One_Batch()
|
||||
{
|
||||
var prices = GeneratePrices(500, seed: 456);
|
||||
var out_ = new double[prices.Length];
|
||||
Mstoch.Batch(prices.AsSpan(), out_.AsSpan(), stochLength: 20, hpLength: 48, ssLength: 10);
|
||||
for (int i = 0; i < out_.Length; i++)
|
||||
{
|
||||
Assert.True(out_[i] >= 0.0 && out_[i] <= 1.0,
|
||||
$"Bar {i}: value {out_[i]} out of [0,1]");
|
||||
}
|
||||
}
|
||||
|
||||
// --- D) Constant input produces finite output (zero range -> midpoint) ---
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_ProducesFiniteOutput()
|
||||
{
|
||||
double[] prices = Enumerable.Repeat(100.0, 100).ToArray();
|
||||
var out_ = new double[100];
|
||||
Mstoch.Batch(prices.AsSpan(), out_.AsSpan(), stochLength: 20, hpLength: 48, ssLength: 10);
|
||||
for (int i = 0; i < out_.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(out_[i]), $"Output[{i}] = {out_[i]} is not finite");
|
||||
}
|
||||
}
|
||||
|
||||
// --- E) Update(TSeries) matches Batch(TSeries) ---
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_Matches_Batch_TSeries()
|
||||
{
|
||||
var prices = GeneratePrices(150);
|
||||
var series = MakeSeries(prices);
|
||||
const int stochLength = 10;
|
||||
const int hpLength = 20;
|
||||
const int ssLength = 5;
|
||||
|
||||
var indicator = new Mstoch(stochLength, hpLength, ssLength);
|
||||
TSeries updateResult = indicator.Update(series);
|
||||
|
||||
TSeries batchResult = Mstoch.Batch(series, stochLength, hpLength, ssLength);
|
||||
|
||||
// All values should match
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], updateResult.Values[i], 6);
|
||||
}
|
||||
}
|
||||
|
||||
// --- F) Calculate static factory returns consistent result ---
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Matches_Batch()
|
||||
{
|
||||
var prices = GeneratePrices(200, seed: 99);
|
||||
var series = MakeSeries(prices);
|
||||
const int stochLength = 20;
|
||||
const int hpLength = 48;
|
||||
const int ssLength = 10;
|
||||
|
||||
var (calcResult, _) = Mstoch.Calculate(series, stochLength, hpLength, ssLength);
|
||||
TSeries batchResult = Mstoch.Batch(series, stochLength, hpLength, ssLength);
|
||||
|
||||
Assert.Equal(batchResult[^1].Value, calcResult[^1].Value, 6);
|
||||
}
|
||||
|
||||
// --- G) Directional correctness ---
|
||||
|
||||
[Fact]
|
||||
public void Rising_Then_Falling_Prices_ShowsDirectionalResponse()
|
||||
{
|
||||
// After enough rising prices, MSTOCH should be above midpoint (0.5)
|
||||
var mstoch = new Mstoch(stochLength: 10, hpLength: 20, ssLength: 5);
|
||||
var t0 = DateTime.UtcNow;
|
||||
|
||||
// Feed 100 warmup bars at constant 100
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(t0.AddSeconds(i), 100.0));
|
||||
}
|
||||
|
||||
// Feed 50 strongly rising bars
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(t0.AddSeconds(100 + i), 100.0 + i * 2.0));
|
||||
}
|
||||
double risingVal = mstoch.Last.Value;
|
||||
|
||||
// Feed 50 strongly falling bars from a new instance reset
|
||||
mstoch.Reset();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(t0.AddSeconds(i), 100.0));
|
||||
}
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
mstoch.Update(new TValue(t0.AddSeconds(100 + i), 100.0 - i * 2.0));
|
||||
}
|
||||
double fallingVal = mstoch.Last.Value;
|
||||
|
||||
// MSTOCH is a cycle indicator based on HP-filtered (detrended) data.
|
||||
// During a strong uptrend, the HP filter output is near its recent high → stochastic near 1.
|
||||
// During a strong downtrend, the HP filter output is near its recent low → stochastic near 0.
|
||||
// The two scenarios must produce distinctly different readings.
|
||||
Assert.NotEqual(risingVal, fallingVal);
|
||||
Assert.True(double.IsFinite(risingVal) && double.IsFinite(fallingVal),
|
||||
$"Both values must be finite: rising={risingVal}, falling={fallingVal}");
|
||||
// Validate they diverge significantly (opposite ends of [0,1])
|
||||
Assert.True(Math.Abs(risingVal - fallingVal) > 0.5,
|
||||
$"Rising ({risingVal}) and falling ({fallingVal}) should diverge by >0.5");
|
||||
}
|
||||
|
||||
// --- H) NaN input self-consistency ---
|
||||
|
||||
[Fact]
|
||||
public void SparseNaN_Streaming_OutputFinite()
|
||||
{
|
||||
var prices = GeneratePrices(100);
|
||||
// Inject some NaNs
|
||||
prices[10] = double.NaN;
|
||||
prices[25] = double.NaN;
|
||||
prices[50] = double.PositiveInfinity;
|
||||
|
||||
var t0 = DateTime.UtcNow;
|
||||
var mstoch = new Mstoch(stochLength: 10, hpLength: 20, ssLength: 5);
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
TValue result = mstoch.Update(new TValue(t0.AddSeconds(i), prices[i]));
|
||||
Assert.True(double.IsFinite(result.Value),
|
||||
$"Bar {i}: NaN/Inf input produced non-finite output {result.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using static System.Math;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// MSTOCH: Ehlers MESA Stochastic.
|
||||
/// Three-stage pipeline: (1) Roofing Filter = 2-pole Butterworth HP + Super Smoother,
|
||||
/// (2) Standard stochastic on roofing-filtered data,
|
||||
/// (3) Super Smoother of stochastic output. Result clamped to [0,1].
|
||||
/// All IIR stages are O(1); only the min/max scan in stage 2 is O(stochLength).
|
||||
/// Reference: John F. Ehlers, "Cycle Analytics for Traders" (2013), Chapter 6.
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Mstoch : ITValuePublisher
|
||||
{
|
||||
private readonly int _stochLength;
|
||||
private readonly int _hpLength;
|
||||
private readonly int _ssLength;
|
||||
|
||||
// Precomputed IIR coefficients (readonly — fixed at construction)
|
||||
private readonly double _hpC1;
|
||||
private readonly double _hpC2;
|
||||
private readonly double _hpC3;
|
||||
private readonly double _ssC1;
|
||||
private readonly double _ssC2;
|
||||
private readonly double _ssC3;
|
||||
|
||||
// Ring buffer for Filt values (stage-2 stochastic window)
|
||||
private readonly double[] _filtBuf;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double Src1, // src[t-1]
|
||||
double Src2, // src[t-2]
|
||||
double Hp1, // HP[t-1]
|
||||
double Hp2, // HP[t-2]
|
||||
double Filt1, // Filt[t-1]
|
||||
double Filt2, // Filt[t-2]
|
||||
double Stoc1, // stoc[t-1] (for stage-3 input average)
|
||||
double Mstoc1, // mstoc[t-1]
|
||||
double Mstoc2, // mstoc[t-2]
|
||||
double LastValidSrc, // NaN substitution
|
||||
int BufHead, // ring buffer write head
|
||||
int Count); // bars seen
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
public string Name { get; }
|
||||
public int WarmupPeriod { get; }
|
||||
public TValue Last { get; private set; }
|
||||
public bool IsHot => _s.Count >= WarmupPeriod;
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
public Mstoch(int stochLength = 20, int hpLength = 48, int ssLength = 10)
|
||||
{
|
||||
if (stochLength < 2)
|
||||
{
|
||||
throw new ArgumentException("Stochastic length must be >= 2", nameof(stochLength));
|
||||
}
|
||||
if (hpLength < 1)
|
||||
{
|
||||
throw new ArgumentException("HP length must be >= 1", nameof(hpLength));
|
||||
}
|
||||
if (ssLength < 1)
|
||||
{
|
||||
throw new ArgumentException("SS length must be >= 1", nameof(ssLength));
|
||||
}
|
||||
|
||||
_stochLength = stochLength;
|
||||
_hpLength = hpLength;
|
||||
_ssLength = ssLength;
|
||||
|
||||
// Precompute HP coefficients
|
||||
double hpArg = Sqrt(2.0) * PI / hpLength;
|
||||
double hpExp = Exp(-hpArg);
|
||||
_hpC2 = 2.0 * hpExp * Cos(hpArg);
|
||||
_hpC3 = -(hpExp * hpExp);
|
||||
_hpC1 = (1.0 + _hpC2 - _hpC3) / 4.0;
|
||||
|
||||
// Precompute Super Smoother coefficients
|
||||
double ssArg = Sqrt(2.0) * PI / ssLength;
|
||||
double ssExp = Exp(-ssArg);
|
||||
_ssC2 = 2.0 * ssExp * Cos(ssArg);
|
||||
_ssC3 = -(ssExp * ssExp);
|
||||
_ssC1 = 1.0 - _ssC2 - _ssC3;
|
||||
|
||||
_filtBuf = new double[stochLength];
|
||||
|
||||
_s = new State(0, 0, 0, 0, 0, 0, 0.5, 0.5, 0.5, double.NaN, 0, 0);
|
||||
_ps = _s;
|
||||
|
||||
Name = $"Mstoch({stochLength},{hpLength},{ssLength})";
|
||||
WarmupPeriod = stochLength + ssLength + 2; // conservative estimate
|
||||
}
|
||||
|
||||
public Mstoch(ITValuePublisher source, int stochLength = 20, int hpLength = 48, int ssLength = 10)
|
||||
: this(stochLength, hpLength, ssLength)
|
||||
{
|
||||
source.Pub += (object? _, in TValueEventArgs 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 });
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
double src = input.Value;
|
||||
if (double.IsFinite(src))
|
||||
{
|
||||
s.LastValidSrc = src;
|
||||
}
|
||||
else
|
||||
{
|
||||
src = double.IsNaN(s.LastValidSrc) ? 0.0 : s.LastValidSrc;
|
||||
}
|
||||
|
||||
// === Stage 1: Highpass (2-pole Butterworth, removes trend) ===
|
||||
// HP = c1*(src - 2*src1 + src2) + c2*hp1 + c3*hp2
|
||||
double hp = Math.FusedMultiplyAdd(
|
||||
_hpC1, src - 2.0 * s.Src1 + s.Src2,
|
||||
Math.FusedMultiplyAdd(_hpC2, s.Hp1, _hpC3 * s.Hp2));
|
||||
|
||||
// === Stage 1: Super Smoother of HP => Filt ===
|
||||
// Filt = c1*(hp + hp1)/2 + c2*filt1 + c3*filt2
|
||||
double filtIn = (hp + s.Hp1) * 0.5;
|
||||
double filt = Math.FusedMultiplyAdd(
|
||||
_ssC1, filtIn,
|
||||
Math.FusedMultiplyAdd(_ssC2, s.Filt1, _ssC3 * s.Filt2));
|
||||
|
||||
// === Stage 2: Stochastic on Filt ring buffer ===
|
||||
int head = s.BufHead;
|
||||
_filtBuf[head] = filt;
|
||||
|
||||
int count = s.Count + (isNew ? 1 : 0);
|
||||
if (isNew)
|
||||
{
|
||||
s.Count = count;
|
||||
s.BufHead = (head + 1) % _stochLength;
|
||||
}
|
||||
|
||||
int filled = Min(count, _stochLength);
|
||||
|
||||
double highestC = filt;
|
||||
double lowestC = filt;
|
||||
int startHead = isNew ? s.BufHead : head; // new head after increment
|
||||
for (int i = 0; i < filled; i++)
|
||||
{
|
||||
int idx = (startHead - 1 - i + _stochLength) % _stochLength;
|
||||
// For isNew path, startHead = new s.BufHead, so idx wraps correctly
|
||||
double val = _filtBuf[idx];
|
||||
if (val > highestC) { highestC = val; }
|
||||
if (val < lowestC) { lowestC = val; }
|
||||
}
|
||||
|
||||
double rangeVal = highestC - lowestC;
|
||||
double stoc = rangeVal > 0.0 ? (filt - lowestC) / rangeVal : 0.5;
|
||||
|
||||
// === Stage 3: Super Smoother of stochastic ===
|
||||
double mstocIn = (stoc + s.Stoc1) * 0.5;
|
||||
double mstoc = Math.FusedMultiplyAdd(
|
||||
_ssC1, mstocIn,
|
||||
Math.FusedMultiplyAdd(_ssC2, s.Mstoc1, _ssC3 * s.Mstoc2));
|
||||
|
||||
double result = Max(0.0, Min(1.0, mstoc));
|
||||
|
||||
// Update state
|
||||
s.Src2 = s.Src1;
|
||||
s.Src1 = src;
|
||||
s.Hp2 = s.Hp1;
|
||||
s.Hp1 = hp;
|
||||
s.Filt2 = s.Filt1;
|
||||
s.Filt1 = filt;
|
||||
s.Stoc1 = stoc;
|
||||
s.Mstoc2 = s.Mstoc1;
|
||||
s.Mstoc1 = mstoc;
|
||||
|
||||
_s = s;
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return new TSeries([], []);
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
Batch(source.Values, vSpan, _stochLength, _hpLength, _ssLength);
|
||||
|
||||
source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
|
||||
|
||||
// Prime internal state for continued streaming from the last bars
|
||||
Reset();
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Array.Clear(_filtBuf);
|
||||
_s = new State(0, 0, 0, 0, 0, 0, 0.5, 0.5, 0.5, double.NaN, 0, 0);
|
||||
_ps = _s;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
// === Static Batch (span) ===
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> src,
|
||||
Span<double> output,
|
||||
int stochLength = 20,
|
||||
int hpLength = 48,
|
||||
int ssLength = 10)
|
||||
{
|
||||
if (stochLength < 2)
|
||||
{
|
||||
throw new ArgumentException("Stochastic length must be >= 2", nameof(stochLength));
|
||||
}
|
||||
if (hpLength < 1)
|
||||
{
|
||||
throw new ArgumentException("HP length must be >= 1", nameof(hpLength));
|
||||
}
|
||||
if (ssLength < 1)
|
||||
{
|
||||
throw new ArgumentException("SS length must be >= 1", nameof(ssLength));
|
||||
}
|
||||
if (output.Length < src.Length)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as input", nameof(output));
|
||||
}
|
||||
|
||||
int len = src.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Precompute coefficients
|
||||
double hpArg = Sqrt(2.0) * PI / hpLength;
|
||||
double hpExp = Exp(-hpArg);
|
||||
double hpC2 = 2.0 * hpExp * Cos(hpArg);
|
||||
double hpC3 = -(hpExp * hpExp);
|
||||
double hpC1 = (1.0 + hpC2 - hpC3) / 4.0;
|
||||
|
||||
double ssArg = Sqrt(2.0) * PI / ssLength;
|
||||
double ssExp = Exp(-ssArg);
|
||||
double ssC2 = 2.0 * ssExp * Cos(ssArg);
|
||||
double ssC3 = -(ssExp * ssExp);
|
||||
double ssC1 = 1.0 - ssC2 - ssC3;
|
||||
|
||||
const int StackallocThreshold = 256;
|
||||
double[]? rentedFilt = null;
|
||||
double[]? rentedBuf = null;
|
||||
scoped Span<double> filtArr;
|
||||
scoped Span<double> filtBuf;
|
||||
|
||||
if (len <= StackallocThreshold)
|
||||
{
|
||||
filtArr = stackalloc double[len];
|
||||
}
|
||||
else
|
||||
{
|
||||
rentedFilt = ArrayPool<double>.Shared.Rent(len);
|
||||
filtArr = rentedFilt.AsSpan(0, len);
|
||||
}
|
||||
|
||||
if (stochLength <= StackallocThreshold)
|
||||
{
|
||||
filtBuf = stackalloc double[stochLength];
|
||||
}
|
||||
else
|
||||
{
|
||||
rentedBuf = ArrayPool<double>.Shared.Rent(stochLength);
|
||||
filtBuf = rentedBuf.AsSpan(0, stochLength);
|
||||
}
|
||||
filtBuf.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
// Pass 1: compute HP + Filt for all bars
|
||||
double prevSrc2 = 0.0, prevSrc1 = 0.0;
|
||||
double prevHp2 = 0.0, prevHp1 = 0.0;
|
||||
double prevFilt2 = 0.0, prevFilt1 = 0.0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double srcVal = src[i];
|
||||
double s;
|
||||
if (double.IsFinite(srcVal))
|
||||
{
|
||||
s = srcVal;
|
||||
}
|
||||
else if (i > 0)
|
||||
{
|
||||
s = src[i - 1];
|
||||
}
|
||||
else
|
||||
{
|
||||
s = 0.0;
|
||||
}
|
||||
|
||||
double hp = Math.FusedMultiplyAdd(
|
||||
hpC1, s - 2.0 * prevSrc1 + prevSrc2,
|
||||
Math.FusedMultiplyAdd(hpC2, prevHp1, hpC3 * prevHp2));
|
||||
|
||||
double filtIn = (hp + prevHp1) * 0.5;
|
||||
double filt = Math.FusedMultiplyAdd(
|
||||
ssC1, filtIn,
|
||||
Math.FusedMultiplyAdd(ssC2, prevFilt1, ssC3 * prevFilt2));
|
||||
|
||||
filtArr[i] = filt;
|
||||
|
||||
prevSrc2 = prevSrc1;
|
||||
prevSrc1 = s;
|
||||
prevHp2 = prevHp1;
|
||||
prevHp1 = hp;
|
||||
prevFilt2 = prevFilt1;
|
||||
prevFilt1 = filt;
|
||||
}
|
||||
|
||||
// Pass 2: stochastic + super smoother
|
||||
int bufHead = 0;
|
||||
double prevStoc1 = 0.5;
|
||||
double prevMstoc2 = 0.5, prevMstoc1 = 0.5;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double filt = filtArr[i];
|
||||
filtBuf[bufHead] = filt;
|
||||
bufHead = (bufHead + 1) % stochLength;
|
||||
|
||||
int filled = Min(i + 1, stochLength);
|
||||
double highestC = filt;
|
||||
double lowestC = filt;
|
||||
for (int k = 0; k < filled; k++)
|
||||
{
|
||||
int idx = (bufHead - 1 - k + stochLength) % stochLength;
|
||||
double val = filtBuf[idx];
|
||||
if (val > highestC) { highestC = val; }
|
||||
if (val < lowestC) { lowestC = val; }
|
||||
}
|
||||
|
||||
double rangeVal = highestC - lowestC;
|
||||
double stoc = rangeVal > 0.0 ? (filt - lowestC) / rangeVal : 0.5;
|
||||
|
||||
double mstocIn = (stoc + prevStoc1) * 0.5;
|
||||
double mstoc = Math.FusedMultiplyAdd(
|
||||
ssC1, mstocIn,
|
||||
Math.FusedMultiplyAdd(ssC2, prevMstoc1, ssC3 * prevMstoc2));
|
||||
|
||||
output[i] = Max(0.0, Min(1.0, mstoc));
|
||||
|
||||
prevStoc1 = stoc;
|
||||
prevMstoc2 = prevMstoc1;
|
||||
prevMstoc1 = mstoc;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedFilt != null)
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedFilt);
|
||||
}
|
||||
if (rentedBuf != null)
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Batch(TSeries source, int stochLength = 20, int hpLength = 48, int ssLength = 10)
|
||||
{
|
||||
if (source == null || source.Count == 0)
|
||||
{
|
||||
return new TSeries([], []);
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
Batch(source.Values, CollectionsMarshal.AsSpan(v), stochLength, hpLength, ssLength);
|
||||
source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public static (TSeries Result, Mstoch Indicator) Calculate(
|
||||
TSeries source, int stochLength = 20, int hpLength = 48, int ssLength = 10)
|
||||
{
|
||||
var indicator = new Mstoch(stochLength, hpLength, ssLength);
|
||||
var result = indicator.Update(source);
|
||||
return (result, indicator);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user