mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 13:38:05 +00:00
docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class MassiIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void MassiIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new MassiIndicator();
|
||||
|
||||
Assert.Equal(9, indicator.EmaLength);
|
||||
Assert.Equal(25, indicator.SumLength);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("MASSI - Mass Index", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MassiIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new MassiIndicator { EmaLength = 10, SumLength = 30 };
|
||||
Assert.Contains("MASSI", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("10", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("30", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MassiIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new MassiIndicator();
|
||||
|
||||
Assert.Equal(0, MassiIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MassiIndicator_Initialize_CreatesInternalMassi()
|
||||
{
|
||||
var indicator = new MassiIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MassiIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MassiIndicator { EmaLength = 5, SumLength = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MassiIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MassiIndicator { EmaLength = 5, SumLength = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(50), 160, 168, 155, 165, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MassiIndicator_DifferentParameters_Work()
|
||||
{
|
||||
int[] emaLengths = { 5, 9, 14 };
|
||||
int[] sumLengths = { 10, 25, 50 };
|
||||
|
||||
foreach (var emaLen in emaLengths)
|
||||
{
|
||||
foreach (var sumLen in sumLengths)
|
||||
{
|
||||
var indicator = new MassiIndicator { EmaLength = emaLen, SumLength = sumLen };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 80; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"MASSI({emaLen},{sumLen}) should produce finite value");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MassiIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new MassiIndicator();
|
||||
Assert.Equal(9, indicator.EmaLength);
|
||||
Assert.Equal(25, indicator.SumLength);
|
||||
|
||||
indicator.EmaLength = 12;
|
||||
indicator.SumLength = 30;
|
||||
Assert.Equal(12, indicator.EmaLength);
|
||||
Assert.Equal(30, indicator.SumLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MassiIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new MassiIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MassiIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new MassiIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Massi.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MassiIndicator_TypicalRange_AroundSumLength()
|
||||
{
|
||||
// With sumLength=25, MASSI typically hovers around 25 (sum of ratios ~1.0 each)
|
||||
var indicator = new MassiIndicator { EmaLength = 9, SumLength = 25 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Use consistent range data
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double basePrice = 100;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 10, basePrice - 10, basePrice, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
// With stable range, ratios approach 1.0, so sum approaches sumLength (25)
|
||||
Assert.True(val > 20 && val < 30, $"MASSI value {val} should be near 25 for stable data");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MassiIndicator_UsesHighLowRange()
|
||||
{
|
||||
var indicator = new MassiIndicator { EmaLength = 5, SumLength = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Small range bars
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 101, 99, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
double smallRangeVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Reset and use large range bars
|
||||
var indicator2 = new MassiIndicator { EmaLength = 5, SumLength = 10 };
|
||||
indicator2.Initialize();
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), 100, 120, 80, 100, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
double largeRangeVal = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Both should produce valid values (MASSI is about ratio patterns, not absolute range)
|
||||
Assert.True(double.IsFinite(smallRangeVal));
|
||||
Assert.True(double.IsFinite(largeRangeVal));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,727 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class MassiTests
|
||||
{
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
private static TBarSeries GenerateTestBars(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
private static TSeries GenerateRangeData(int count = 100)
|
||||
{
|
||||
var bars = GenerateTestBars(count);
|
||||
var series = new TSeries(count);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
series.Add(new TValue(bars[i].Time, bars[i].High - bars[i].Low));
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
// ============== Constructor & Parameter Validation ==============
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Massi(0, 25));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Massi(-1, 25));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Massi(9, 0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Massi(9, -1));
|
||||
|
||||
var massi = new Massi(9, 25);
|
||||
Assert.NotNull(massi);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsCorrectName()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
Assert.Equal("Massi(9,25)", massi.Name);
|
||||
Assert.True(massi.WarmupPeriod > 0);
|
||||
|
||||
var massi2 = new Massi(5, 10);
|
||||
Assert.Equal("Massi(5,10)", massi2.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsCorrectWarmup()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
// Warmup = emaLength + sumLength
|
||||
Assert.Equal(34, massi.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ============== Basic Functionality ==============
|
||||
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
var bars = GenerateTestBars(100);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
massi.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(massi.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValidValue()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
var bars = GenerateTestBars(50);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = massi.Update(bar);
|
||||
Assert.True(double.IsFinite(result.Value) || double.IsNaN(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
|
||||
Assert.False(massi.IsHot);
|
||||
Assert.Contains("Massi", massi.Name, StringComparison.Ordinal);
|
||||
Assert.Equal(34, massi.WarmupPeriod);
|
||||
|
||||
var bars = GenerateTestBars(50);
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
massi.Update(bar);
|
||||
}
|
||||
|
||||
// After warmup, properties should be valid
|
||||
Assert.True(double.IsFinite(massi.Ema1));
|
||||
Assert.True(double.IsFinite(massi.Ema2));
|
||||
Assert.True(double.IsFinite(massi.Ratio));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmaProperties_Accessible()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
var bars = GenerateTestBars(50);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
massi.Update(bar);
|
||||
}
|
||||
|
||||
// Ema1 and Ema2 should be positive (ranges are positive)
|
||||
Assert.True(massi.Ema1 >= 0);
|
||||
Assert.True(massi.Ema2 >= 0);
|
||||
// Ratio should be close to 1 normally
|
||||
Assert.True(massi.Ratio >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ratio_IsCloseToOne()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
var bars = GenerateTestBars(100);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
massi.Update(bar);
|
||||
}
|
||||
|
||||
// EMA1/EMA2 ratio typically oscillates around 1
|
||||
// For stable conditions, should be between 0.5 and 2.0
|
||||
Assert.True(massi.Ratio > 0.5 && massi.Ratio < 2.0,
|
||||
$"Ratio {massi.Ratio} outside expected range");
|
||||
}
|
||||
|
||||
// ============== State Management & Bar Correction ==============
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
var bars = GenerateTestBars(50);
|
||||
|
||||
// Feed enough values to build up state
|
||||
for (int i = 0; i < 49; i++)
|
||||
{
|
||||
massi.Update(bars[i], isNew: true);
|
||||
}
|
||||
double valueBefore = massi.Last.Value;
|
||||
|
||||
// Add one more value with isNew=true
|
||||
massi.Update(bars[49], isNew: true);
|
||||
double valueAfter = massi.Last.Value;
|
||||
|
||||
// Both should be valid
|
||||
Assert.True(double.IsFinite(valueBefore));
|
||||
Assert.True(double.IsFinite(valueAfter));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
var bars = GenerateTestBars(50);
|
||||
|
||||
// Feed enough bars
|
||||
for (int i = 0; i < 49; i++)
|
||||
{
|
||||
massi.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Add one more value with isNew=true
|
||||
massi.Update(bars[49], isNew: true);
|
||||
double beforeUpdate = massi.Last.Value;
|
||||
|
||||
// Update same bar with different value (isNew=false)
|
||||
var modifiedBar = new TBar(bars[49].Time, bars[49].Open, bars[49].High + 5,
|
||||
bars[49].Low - 5, bars[49].Close, bars[49].Volume);
|
||||
massi.Update(modifiedBar, isNew: false);
|
||||
double afterUpdate = massi.Last.Value;
|
||||
|
||||
// Values should be different after the correction (wider range)
|
||||
Assert.True(Math.Abs(beforeUpdate - afterUpdate) > Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
var bars = GenerateTestBars(100);
|
||||
|
||||
// Feed first 99
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
massi.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Update with 100th bar (isNew=true)
|
||||
massi.Update(bars[99], true);
|
||||
|
||||
// Update with modified 100th bar (isNew=false)
|
||||
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 5,
|
||||
bars[99].Low - 5, bars[99].Close, bars[99].Volume);
|
||||
double val2 = massi.Update(modifiedBar, false).Value;
|
||||
|
||||
// Create new instance and feed up to modified
|
||||
var massi2 = new Massi(9, 25);
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
massi2.Update(bars[i]);
|
||||
}
|
||||
double val3 = massi2.Update(modifiedBar, true).Value;
|
||||
|
||||
Assert.Equal(val3, val2, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var massi = new Massi(5, 10);
|
||||
var bars = GenerateTestBars(20);
|
||||
|
||||
// Feed 10 new values
|
||||
TBar tenthBar = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tenthBar = bars[i];
|
||||
massi.Update(tenthBar, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = massi.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 10; i < 19; i++)
|
||||
{
|
||||
massi.Update(bars[i], isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th bar again with isNew=false
|
||||
TValue finalResult = massi.Update(tenthBar, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
Assert.Equal(stateAfterTen, finalResult.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_Works()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
var bars = GenerateTestBars(50);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
massi.Update(bar);
|
||||
}
|
||||
|
||||
massi.Reset();
|
||||
Assert.False(massi.IsHot);
|
||||
Assert.Equal(0.0, massi.Ema1, Tolerance);
|
||||
Assert.Equal(0.0, massi.Ema2, Tolerance);
|
||||
Assert.Equal(0.0, massi.Ratio, Tolerance);
|
||||
}
|
||||
|
||||
// ============== Warmup & Convergence ==============
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
|
||||
Assert.False(massi.IsHot);
|
||||
|
||||
var bars = GenerateTestBars(50);
|
||||
|
||||
int steps = 0;
|
||||
while (!massi.IsHot && steps < bars.Count)
|
||||
{
|
||||
massi.Update(bars[steps]);
|
||||
steps++;
|
||||
}
|
||||
|
||||
Assert.True(massi.IsHot);
|
||||
Assert.Equal(34, steps); // emaLength + sumLength
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsPositive()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
Assert.Equal(34, massi.WarmupPeriod);
|
||||
|
||||
var massi2 = new Massi(5, 10);
|
||||
Assert.Equal(15, massi2.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ============== NaN/Infinity Handling ==============
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var massi = new Massi(5, 10);
|
||||
var bars = GenerateTestBars(20);
|
||||
|
||||
// Feed some valid bars
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
massi.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Feed NaN value via TValue (range input)
|
||||
var inputWithNaN = new TValue(DateTime.UtcNow.AddMinutes(20), double.NaN);
|
||||
var resultAfterNaN = massi.Update(inputWithNaN);
|
||||
|
||||
// Result should be finite
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var massi = new Massi(5, 10);
|
||||
var bars = GenerateTestBars(20);
|
||||
|
||||
// Feed some valid bars
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
massi.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Feed Infinity value
|
||||
var inputWithInf = new TValue(DateTime.UtcNow.AddMinutes(20), double.PositiveInfinity);
|
||||
var resultAfterInf = massi.Update(inputWithInf);
|
||||
|
||||
// Result should be finite
|
||||
Assert.True(double.IsFinite(resultAfterInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegativeRange_UsesLastValidValue()
|
||||
{
|
||||
var massi = new Massi(5, 10);
|
||||
var bars = GenerateTestBars(20);
|
||||
|
||||
// Feed some valid bars
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
massi.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Feed negative range value (invalid)
|
||||
var inputWithNeg = new TValue(DateTime.UtcNow.AddMinutes(20), -5.0);
|
||||
var resultAfterNeg = massi.Update(inputWithNeg);
|
||||
|
||||
// Result should be finite
|
||||
Assert.True(double.IsFinite(resultAfterNeg.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_Safe()
|
||||
{
|
||||
var massi = new Massi(5, 10);
|
||||
var bars = GenerateTestBars(20);
|
||||
|
||||
// Feed some values
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
massi.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Feed multiple NaN values
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var nanInput = new TValue(DateTime.UtcNow.AddMinutes(15 + i), double.NaN);
|
||||
var result = massi.Update(nanInput);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Consistency Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void TBarSeries_MatchesIterativeCalc()
|
||||
{
|
||||
var massiIterative = new Massi(9, 25);
|
||||
var bars = GenerateTestBars(100);
|
||||
|
||||
// Calculate iteratively
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
massiIterative.Update(bar);
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var massiBatch = new Massi(9, 25);
|
||||
_ = massiBatch.Update(bars);
|
||||
|
||||
// Compare last values
|
||||
Assert.Equal(massiIterative.Last.Value, massiBatch.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TSeries_Update_MatchesStreaming()
|
||||
{
|
||||
var massi1 = new Massi(9, 25);
|
||||
var massi2 = new Massi(9, 25);
|
||||
var series = GenerateRangeData(100);
|
||||
|
||||
// Streaming
|
||||
foreach (var value in series)
|
||||
{
|
||||
massi1.Update(value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
massi2.Update(series);
|
||||
|
||||
Assert.Equal(massi1.Last.Value, massi2.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalc_MatchesStreaming()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
var series = GenerateRangeData(100);
|
||||
|
||||
// Stream all values first
|
||||
foreach (var value in series)
|
||||
{
|
||||
massi.Update(value);
|
||||
}
|
||||
double streamingLast = massi.Last.Value;
|
||||
|
||||
// Span calculation
|
||||
var output = new double[series.Count];
|
||||
Massi.Batch(series.Values, output, 9, 25);
|
||||
|
||||
// Compare last value
|
||||
Assert.Equal(streamingLast, output[series.Count - 1], 1e-6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
var bars = GenerateTestBars(50);
|
||||
|
||||
var result = massi.Update(bars);
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.Equal(massi.Last.Value, result.Last.Value);
|
||||
}
|
||||
|
||||
// ============== Mass Index Specific Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void Massi_TypicalRange()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
var bars = GenerateTestBars(200);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
massi.Update(bar);
|
||||
}
|
||||
|
||||
// Mass Index sum typically ranges around 25 (sumLength * 1.0 ratio)
|
||||
// During normal conditions, expect 20-30 range
|
||||
Assert.True(massi.Last.Value > 10, $"MASSI {massi.Last.Value} unexpectedly low");
|
||||
Assert.True(massi.Last.Value < 40, $"MASSI {massi.Last.Value} unexpectedly high");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Massi_SumLengthAffectsOutput()
|
||||
{
|
||||
var massi10 = new Massi(9, 10);
|
||||
var massi25 = new Massi(9, 25);
|
||||
var massi50 = new Massi(9, 50);
|
||||
|
||||
var bars = GenerateTestBars(100);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
massi10.Update(bar);
|
||||
massi25.Update(bar);
|
||||
massi50.Update(bar);
|
||||
}
|
||||
|
||||
// Longer sumLength should produce larger output (more terms summed)
|
||||
Assert.True(massi25.Last.Value > massi10.Last.Value,
|
||||
$"MASSI(9,25)={massi25.Last.Value} should be > MASSI(9,10)={massi10.Last.Value}");
|
||||
Assert.True(massi50.Last.Value > massi25.Last.Value,
|
||||
$"MASSI(9,50)={massi50.Last.Value} should be > MASSI(9,25)={massi25.Last.Value}");
|
||||
}
|
||||
|
||||
// ============== Static Batch Methods ==============
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_TBarSeries_Works()
|
||||
{
|
||||
var bars = GenerateTestBars(50);
|
||||
|
||||
var results = Massi.Batch(bars, 9, 25);
|
||||
|
||||
Assert.Equal(50, results.Count);
|
||||
Assert.True(double.IsFinite(results.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_TSeries_Works()
|
||||
{
|
||||
var series = GenerateRangeData(50);
|
||||
|
||||
var results = Massi.Batch(series, 9, 25);
|
||||
|
||||
Assert.Equal(50, results.Count);
|
||||
Assert.True(double.IsFinite(results.Last.Value));
|
||||
}
|
||||
|
||||
// ============== Span API Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ValidatesLengths()
|
||||
{
|
||||
var source = new double[10];
|
||||
var output = new double[5]; // Wrong size
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Massi.Batch(source, output, 9, 25));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_EmptySource_NoException()
|
||||
{
|
||||
var source = Array.Empty<double>();
|
||||
var output = Array.Empty<double>();
|
||||
|
||||
var exception = Record.Exception(() => Massi.Batch(source, output, 9, 25));
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_InvalidParams_ThrowsArgumentException()
|
||||
{
|
||||
var source = new double[10];
|
||||
var output = new double[10];
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => Massi.Batch(source, output, 0, 25));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => Massi.Batch(source, output, 9, 0));
|
||||
}
|
||||
|
||||
// ============== Edge Cases ==============
|
||||
|
||||
[Fact]
|
||||
public void SingleValue_ReturnsValue()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
var bar = GenerateTestBars(1)[0];
|
||||
|
||||
var result = massi.Update(bar);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Period1_Works()
|
||||
{
|
||||
var massi = new Massi(1, 1);
|
||||
var bars = GenerateTestBars(10);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = massi.Update(bar);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FlatRange_ProducesStableOutput()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
|
||||
// All bars have same range
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i).Ticks, 100.0, 101.0, 99.0, 100.5, 1000.0);
|
||||
massi.Update(bar);
|
||||
}
|
||||
|
||||
// Output should be approximately sumLength (25) since ratio ≈ 1
|
||||
Assert.True(Math.Abs(massi.Last.Value - 25.0) < 1.0,
|
||||
$"MASSI {massi.Last.Value} should be close to 25 for flat range");
|
||||
}
|
||||
|
||||
// ============== Event Publishing ==============
|
||||
|
||||
[Fact]
|
||||
public void PubEvent_Fires()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
bool eventFired = false;
|
||||
|
||||
massi.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
||||
|
||||
var bar = GenerateTestBars(1)[0];
|
||||
massi.Update(bar);
|
||||
|
||||
Assert.True(eventFired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventChaining_Works()
|
||||
{
|
||||
var bars = GenerateTestBars(50);
|
||||
|
||||
var massi = new Massi(9, 25);
|
||||
var sma = new Sma(massi, 5); // Chain SMA to MASSI output
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
massi.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(sma.Last.Value));
|
||||
}
|
||||
|
||||
// ============== Additional Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_Completes()
|
||||
{
|
||||
var massi = new Massi(9, 25);
|
||||
var bars = GenerateTestBars(5000);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
massi.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(massi.IsHot);
|
||||
Assert.True(double.IsFinite(massi.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentParameters_ProduceValidValues()
|
||||
{
|
||||
var bars = GenerateTestBars(200);
|
||||
|
||||
var massi1 = new Massi(5, 10);
|
||||
var massi2 = new Massi(9, 25);
|
||||
var massi3 = new Massi(15, 50);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
massi1.Update(bar);
|
||||
massi2.Update(bar);
|
||||
massi3.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(massi1.Last.Value));
|
||||
Assert.True(double.IsFinite(massi2.Last.Value));
|
||||
Assert.True(double.IsFinite(massi3.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceChaining_Works()
|
||||
{
|
||||
var bars = GenerateTestBars(200);
|
||||
|
||||
// Create source TSeries that publishes events
|
||||
var sourceSeries = new TSeries();
|
||||
var massi = new Massi(sourceSeries, 9, 25);
|
||||
|
||||
// Feed data through the source (as range values)
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
sourceSeries.Add(new TValue(bar.Time, bar.High - bar.Low));
|
||||
}
|
||||
|
||||
// Should have valid output
|
||||
Assert.True(double.IsFinite(massi.Last.Value));
|
||||
}
|
||||
|
||||
#pragma warning disable S2699 // Test contains Assert.True and Assert.InRange - analyzer false positive
|
||||
[Fact]
|
||||
public void Prime_Works()
|
||||
{
|
||||
var massi = new Massi(5, 10);
|
||||
var values = new double[] { 1.0, 1.1, 0.9, 1.2, 0.8, 1.3, 1.0, 1.1, 0.95, 1.05 };
|
||||
|
||||
massi.Prime(values);
|
||||
|
||||
double lastValue = massi.Last.Value;
|
||||
Assert.True(double.IsFinite(lastValue), "Last value should be finite after Prime");
|
||||
}
|
||||
#pragma warning restore S2699
|
||||
|
||||
[Fact]
|
||||
public void TValueUpdate_TreatsValueAsRange()
|
||||
{
|
||||
var massi1 = new Massi(9, 25);
|
||||
var massi2 = new Massi(9, 25);
|
||||
|
||||
var bars = GenerateTestBars(50);
|
||||
|
||||
// Update with TBar
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
massi1.Update(bar);
|
||||
}
|
||||
|
||||
// Update with TValue (pre-calculated range)
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var range = bar.High - bar.Low;
|
||||
massi2.Update(new TValue(bar.Time, range));
|
||||
}
|
||||
|
||||
// Both should produce same result
|
||||
Assert.Equal(massi1.Last.Value, massi2.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// Massi: Mathematical property validation tests
|
||||
// Mass Index by Donald Dorsey. While Ooples has GetMassIndex(), the implementation
|
||||
// differences (EMA compensation, continuous vs discrete sum) make direct comparison
|
||||
// unreliable. Validation uses mathematical property testing instead.
|
||||
|
||||
using Tulip;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
using Xunit;
|
||||
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
|
||||
public class MassiValidationTests
|
||||
{
|
||||
private const int DefaultEmaLength = 9;
|
||||
private const int DefaultSumLength = 25;
|
||||
private const int TestDataLength = 500;
|
||||
|
||||
[Fact]
|
||||
public void Massi_Output_IsFiniteForGbmData()
|
||||
{
|
||||
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var massi = new Massi(DefaultEmaLength, DefaultSumLength);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = massi.Update(bars[i], isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value),
|
||||
$"Massi output must be finite at bar {i}, got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Massi_Output_IsPositive_AfterWarmup()
|
||||
{
|
||||
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var massi = new Massi(DefaultEmaLength, DefaultSumLength);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = massi.Update(bars[i], isNew: true);
|
||||
if (massi.IsHot)
|
||||
{
|
||||
Assert.True(result.Value > 0,
|
||||
$"Massi output must be positive after warmup at bar {i}, got {result.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Massi_ConstantRange_ConvergesToSumLength()
|
||||
{
|
||||
// When High-Low is constant, EMA1 = EMA2 after convergence,
|
||||
// so ratio = 1.0. Sum of 25 ratios = 25.0.
|
||||
var massi = new Massi(DefaultEmaLength, DefaultSumLength);
|
||||
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
101, 101, 99, 100, 1000); // constant range = 2
|
||||
massi.Update(bar, isNew: true);
|
||||
}
|
||||
|
||||
// After convergence: ratio ≈ 1.0, sum ≈ 25.0
|
||||
Assert.Equal(DefaultSumLength, massi.Last.Value, tolerance: 0.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Massi_Ratio_ConvergesToOne_ForConstantRange()
|
||||
{
|
||||
var massi = new Massi(DefaultEmaLength, DefaultSumLength);
|
||||
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
102, 102, 98, 100, 1000);
|
||||
massi.Update(bar, isNew: true);
|
||||
}
|
||||
|
||||
// EMA1/EMA2 should converge to 1.0 for constant range
|
||||
Assert.Equal(1.0, massi.Ratio, precision: 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Massi_Ema1_GreaterThanZero_ForPositiveRange()
|
||||
{
|
||||
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var massi = new Massi(DefaultEmaLength, DefaultSumLength);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
massi.Update(bars[i], isNew: true);
|
||||
if (massi.IsHot)
|
||||
{
|
||||
Assert.True(massi.Ema1 > 0,
|
||||
$"EMA1 must be > 0 at bar {i}, got {massi.Ema1}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Massi_Ema2_GreaterThanZero_ForPositiveRange()
|
||||
{
|
||||
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var massi = new Massi(DefaultEmaLength, DefaultSumLength);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
massi.Update(bars[i], isNew: true);
|
||||
if (massi.IsHot)
|
||||
{
|
||||
Assert.True(massi.Ema2 > 0,
|
||||
$"EMA2 must be > 0 at bar {i}, got {massi.Ema2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Massi_BatchTBarSeries_MatchesStreaming()
|
||||
{
|
||||
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Batch
|
||||
var batchResults = Massi.Batch(bars, DefaultEmaLength, DefaultSumLength);
|
||||
|
||||
// Streaming
|
||||
var streamMassi = new Massi(DefaultEmaLength, DefaultSumLength);
|
||||
var streamResults = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = streamMassi.Update(bars[i], isNew: true);
|
||||
streamResults[i] = result.Value;
|
||||
}
|
||||
|
||||
Assert.Equal(batchResults.Count, bars.Count);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResults.Values[i], streamResults[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Massi_WideningRange_IncreasesValue()
|
||||
{
|
||||
var massi = new Massi(DefaultEmaLength, DefaultSumLength);
|
||||
|
||||
// Start with constant narrow range
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
100.5, 100.5, 99.5, 100, 1000); // range = 1
|
||||
massi.Update(bar, isNew: true);
|
||||
}
|
||||
double narrowValue = massi.Last.Value;
|
||||
|
||||
// Abruptly widen the range
|
||||
for (int i = 100; i < 150; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
110, 110, 90, 100, 1000); // range = 20
|
||||
massi.Update(bar, isNew: true);
|
||||
}
|
||||
double wideValue = massi.Last.Value;
|
||||
|
||||
// Widening range causes EMA1 to react faster than EMA2,
|
||||
// so ratio > 1 and MASSI increases
|
||||
Assert.True(wideValue > narrowValue,
|
||||
$"Widening range should increase MASSI: narrow={narrowValue}, wide={wideValue}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Massi_DifferentParameters_ProduceDifferentResults()
|
||||
{
|
||||
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var massi1 = new Massi(9, 25);
|
||||
var massi2 = new Massi(5, 10);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
massi1.Update(bars[i], isNew: true);
|
||||
massi2.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.NotEqual(massi1.Last.Value, massi2.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Massi_BarCorrection_IsNewFalse_RestoresState()
|
||||
{
|
||||
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var massi = new Massi(DefaultEmaLength, DefaultSumLength);
|
||||
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
massi.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
massi.Update(bars[40], isNew: true);
|
||||
double afterNew = massi.Last.Value;
|
||||
|
||||
massi.Update(bars[40], isNew: false);
|
||||
double afterCorrection = massi.Last.Value;
|
||||
|
||||
Assert.Equal(afterNew, afterCorrection, precision: 10);
|
||||
}
|
||||
|
||||
// === Tulip Cross-Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Structural validation against Tulip <c>mass</c> indicator.
|
||||
/// Algorithm variant: Tulip <c>mass</c> uses a single <c>period</c> for both the EMA
|
||||
/// smoothing window and the summation window (25 bars hardcoded in some builds).
|
||||
/// QuanTAlib uses separate <c>emaLength</c> and <c>sumLength</c> parameters.
|
||||
/// Direct numeric equality is not asserted; test documents the difference and
|
||||
/// verifies both implementations produce finite, positive output on the same data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Massi_Tulip_StructuralVariant_BothFinite()
|
||||
{
|
||||
const int period = 9;
|
||||
var bars = new GBM(sigma: 0.3, seed: 42).Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
double[] highData = new double[bars.Count];
|
||||
double[] lowData = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
highData[i] = bars[i].High;
|
||||
lowData[i] = bars[i].Low;
|
||||
}
|
||||
|
||||
// Tulip mass — single period (covers both EMA pass and sum window)
|
||||
var tulipIndicator = Tulip.Indicators.mass;
|
||||
double[][] inputs = { highData, lowData };
|
||||
double[] options = { period };
|
||||
int lookback = tulipIndicator.Start(options);
|
||||
double[][] outputs = { new double[highData.Length - lookback] };
|
||||
tulipIndicator.Run(inputs, options, outputs);
|
||||
double[] tResult = outputs[0];
|
||||
|
||||
// QuanTAlib Massi — separate emaLength / sumLength
|
||||
var massi = new Massi(emaLength: period, sumLength: DefaultSumLength);
|
||||
foreach (var bar in bars) { massi.Update(bar); }
|
||||
|
||||
// Structural: Tulip must produce finite, positive output
|
||||
Assert.True(tResult.Length > 0, "Tulip mass must produce output");
|
||||
foreach (double v in tResult)
|
||||
{
|
||||
Assert.True(double.IsFinite(v), $"Tulip mass produced non-finite value: {v}");
|
||||
Assert.True(v > 0, $"Mass Index must be positive, got {v}");
|
||||
}
|
||||
|
||||
Assert.True(massi.IsHot, "QuanTAlib Massi must be hot after sufficient bars");
|
||||
Assert.True(massi.Last.Value > 0, "QuanTAlib Massi last value must be positive");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Massi_MatchesOoples_Structural()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var ooplesData = bars.Select(b => new TickerData
|
||||
{
|
||||
Date = new DateTime(b.Time, DateTimeKind.Utc),
|
||||
Open = b.Open, High = b.High, Low = b.Low,
|
||||
Close = b.Close, Volume = b.Volume
|
||||
}).ToList();
|
||||
var result = new StockData(ooplesData).CalculateMassIndex();
|
||||
var values = result.CustomValuesList;
|
||||
int finiteCount = values.Count(v => double.IsFinite(v));
|
||||
Assert.True(finiteCount > 100, $"Expected >100 finite values, got {finiteCount}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user