style patterns

This commit is contained in:
Miha Kralj
2026-01-25 16:01:45 -08:00
parent 2836f253c4
commit e59665c8f0
399 changed files with 6892 additions and 1323 deletions
+4
View File
@@ -37,6 +37,10 @@ dotnet_diagnostic.S3236.severity = none
dotnet_diagnostic.MA0046.severity = none
dotnet_diagnostic.MA0003.severity = suggestion
# Require curly braces on control structures
csharp_prefer_braces = true:warning
dotnet_diagnostic.IDE0011.severity = warning
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = false:silent
+4 -1
View File
@@ -73,7 +73,10 @@ public sealed class AbberIndicator : Indicator, IWatchlistIndicator
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
if (HistoricalData.Count == 0 || _abber is null || _selector is null) return;
if (HistoricalData.Count == 0 || _abber is null || _selector is null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
+1 -1
View File
@@ -421,4 +421,4 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
output.WriteLine($"Abber consistency across {periods.Length} periods validated successfully");
}
}
}
+32 -8
View File
@@ -93,9 +93,14 @@ public sealed class Abber : ITValuePublisher
public Abber(int period, double multiplier = 2.0)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (multiplier <= 0)
{
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
}
_period = period;
_multiplier = multiplier;
@@ -235,7 +240,9 @@ public sealed class Abber : ITValuePublisher
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -302,7 +309,10 @@ public sealed class Abber : ITValuePublisher
/// </summary>
public void Prime(TSeries source)
{
if (source.Count == 0) return;
if (source.Count == 0)
{
return;
}
// Reset state
_sourceBuffer.Clear();
@@ -483,13 +493,24 @@ public sealed class Abber : ITValuePublisher
{
int len = source.Length;
if (middle.Length < len || upper.Length < len || lower.Length < len)
{
throw new ArgumentException("Output buffers must be at least as long as input", nameof(middle));
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (multiplier <= 0)
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
}
if (len == 0) return;
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (multiplier <= 0)
{
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
}
if (len == 0)
{
return;
}
// Scalar implementation with NaN handling
var outputs = new BatchOutputs(middle, upper, lower);
@@ -626,7 +647,10 @@ public sealed class Abber : ITValuePublisher
buffers.Deviation[state.BufferIndex] = deviation;
state.BufferIndex++;
if (state.BufferIndex >= period) state.BufferIndex = 0;
if (state.BufferIndex >= period)
{
state.BufferIndex = 0;
}
double middle = state.SumSource / period;
double avgDeviation = state.SumDeviation / period;
@@ -649,4 +673,4 @@ public sealed class Abber : ITValuePublisher
var results = abber.Update(source);
return (results, abber);
}
}
}
@@ -189,4 +189,4 @@ public class AccBandsIndicatorTests
indicator.Factor = 3.5;
Assert.Equal(3.5, indicator.Factor);
}
}
}
+5 -2
View File
@@ -51,7 +51,10 @@ public sealed class AccBandsIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_accBands == null) return;
if (_accBands == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
@@ -78,4 +81,4 @@ public sealed class AccBandsIndicator : Indicator, IWatchlistIndicator
// Lower band
LinesSeries[2].SetValue(_accBands.Lower.Value, isHot, ShowColdValues);
}
}
}
+101 -14
View File
@@ -96,9 +96,14 @@ public sealed class AccBands : ITValuePublisher, IDisposable
public AccBands(int period, double factor = 2.0)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (factor <= 0)
{
throw new ArgumentException("Factor must be greater than 0", nameof(factor));
}
_period = period;
_factor = factor;
@@ -125,7 +130,11 @@ public sealed class AccBands : ITValuePublisher, IDisposable
/// </summary>
public void Dispose()
{
if (_disposed) return;
if (_disposed)
{
return;
}
_disposed = true;
if (_source != null)
@@ -271,7 +280,9 @@ public sealed class AccBands : ITValuePublisher, IDisposable
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -315,7 +326,10 @@ public sealed class AccBands : ITValuePublisher, IDisposable
// skipcq: CS-R1140
public void Prime(TBarSeries source)
{
if (source.Count == 0) return;
if (source.Count == 0)
{
return;
}
// Reset state
_highBuffer.Clear();
@@ -336,13 +350,24 @@ public sealed class AccBands : ITValuePublisher, IDisposable
{
var bar = source[i];
if (double.IsFinite(bar.High) && double.IsNaN(_state.LastValidHigh))
{
_state.LastValidHigh = bar.High;
}
if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow))
{
_state.LastValidLow = bar.Low;
}
if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose))
{
_state.LastValidClose = bar.Close;
}
if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose))
{
break;
}
}
// Find valid values in warmup window if not found
@@ -352,13 +377,24 @@ public sealed class AccBands : ITValuePublisher, IDisposable
{
var bar = source[i];
if (double.IsFinite(bar.High) && double.IsNaN(_state.LastValidHigh))
{
_state.LastValidHigh = bar.High;
}
if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow))
{
_state.LastValidLow = bar.Low;
}
if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose))
{
_state.LastValidClose = bar.Close;
}
if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose))
{
break;
}
}
}
@@ -578,15 +614,29 @@ public sealed class AccBands : ITValuePublisher, IDisposable
{
int len = close.Length;
if (high.Length != len || low.Length != len)
{
throw new ArgumentException("High, Low, and Close must have the same length", nameof(high));
if (middle.Length < len || upper.Length < len || lower.Length < len)
throw new ArgumentException("Output buffers must be at least as long as input", nameof(middle));
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (factor <= 0)
throw new ArgumentException("Factor must be greater than 0", nameof(factor));
}
if (len == 0) return;
if (middle.Length < len || upper.Length < len || lower.Length < len)
{
throw new ArgumentException("Output buffers must be at least as long as input", nameof(middle));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (factor <= 0)
{
throw new ArgumentException("Factor must be greater than 0", nameof(factor));
}
if (len == 0)
{
return;
}
// Scalar implementation with NaN handling
var inputs = new BatchInputs(high, low, close);
@@ -643,13 +693,24 @@ public sealed class AccBands : ITValuePublisher, IDisposable
for (int k = 0; k < len; k++)
{
if (double.IsFinite(inputs.High[k]) && double.IsNaN(state.LastValidHigh))
{
state.LastValidHigh = inputs.High[k];
}
if (double.IsFinite(inputs.Low[k]) && double.IsNaN(state.LastValidLow))
{
state.LastValidLow = inputs.Low[k];
}
if (double.IsFinite(inputs.Close[k]) && double.IsNaN(state.LastValidClose))
{
state.LastValidClose = inputs.Close[k];
}
if (!double.IsNaN(state.LastValidHigh) && !double.IsNaN(state.LastValidLow) && !double.IsNaN(state.LastValidClose))
{
break;
}
}
}
@@ -660,9 +721,32 @@ public sealed class AccBands : ITValuePublisher, IDisposable
double l = inputs.Low[i];
double c = inputs.Close[i];
if (double.IsFinite(h)) state.LastValidHigh = h; else h = state.LastValidHigh;
if (double.IsFinite(l)) state.LastValidLow = l; else l = state.LastValidLow;
if (double.IsFinite(c)) state.LastValidClose = c; else c = state.LastValidClose;
if (double.IsFinite(h))
{
state.LastValidHigh = h;
}
else
{
h = state.LastValidHigh;
}
if (double.IsFinite(l))
{
state.LastValidLow = l;
}
else
{
l = state.LastValidLow;
}
if (double.IsFinite(c))
{
state.LastValidClose = c;
}
else
{
c = state.LastValidClose;
}
return (h, l, c);
}
@@ -726,7 +810,10 @@ public sealed class AccBands : ITValuePublisher, IDisposable
buffers.Close[state.BufferIndex] = c;
state.BufferIndex++;
if (state.BufferIndex >= period) state.BufferIndex = 0;
if (state.BufferIndex >= period)
{
state.BufferIndex = 0;
}
WriteBandOutputs(outputs, i, state.SumHigh / period, state.SumLow / period, state.SumClose / period, factor);
@@ -759,4 +846,4 @@ public sealed class AccBands : ITValuePublisher, IDisposable
var results = accBands.Update(source);
return (results, accBands);
}
}
}
@@ -217,4 +217,4 @@ public class ApchannelIndicatorTests
double expectedMiddle = (upper + lower) / 2.0;
Assert.Equal(expectedMiddle, middle, 6); // 6 decimal precision
}
}
}
@@ -48,7 +48,10 @@ public sealed class ApchannelIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_apchannel == null) return;
if (_apchannel == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
@@ -75,4 +78,4 @@ public sealed class ApchannelIndicator : Indicator, IWatchlistIndicator
// Lower band
LinesSeries[2].SetValue(_apchannel.LowerBand, isHot, ShowColdValues);
}
}
}
@@ -22,9 +22,16 @@ public sealed class ApchannelValidationTests : IDisposable
private void Dispose(bool disposing)
{
if (_disposed) return;
if (_disposed)
{
return;
}
_disposed = true;
if (disposing) _testData?.Dispose();
if (disposing)
{
_testData?.Dispose();
}
}
/// <summary>
+35 -6
View File
@@ -83,7 +83,11 @@ public sealed class Apchannel : AbstractBase
/// </summary>
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (_disposed)
{
return;
}
_disposed = true;
if (disposing && _source != null)
@@ -174,7 +178,10 @@ public sealed class Apchannel : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -207,7 +214,10 @@ public sealed class Apchannel : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -234,7 +244,9 @@ public sealed class Apchannel : AbstractBase
{
Init();
if (source.Length == 0)
{
return;
}
long time = DateTime.UtcNow.Ticks;
long dt = step?.Ticks ?? TimeSpan.TicksPerMinute;
@@ -281,9 +293,15 @@ public sealed class Apchannel : AbstractBase
int length = sourceHigh.Length;
if (sourceLow.Length != length)
{
throw new ArgumentException("Source arrays must have the same length.", nameof(sourceLow));
}
if (upperBand.Length != length)
{
throw new ArgumentException("Upper band array must match source length.", nameof(upperBand));
}
if (lowerBand.Length != length)
{
throw new ArgumentException("Lower band array must match source length.", nameof(lowerBand));
@@ -295,7 +313,9 @@ public sealed class Apchannel : AbstractBase
}
if (length == 0)
{
return;
}
double decay = 1.0 - alpha;
@@ -324,7 +344,9 @@ public sealed class Apchannel : AbstractBase
// Early return for single-element arrays
if (length == 1)
{
return;
}
for (int i = 1; i < length; i++)
{
@@ -332,8 +354,15 @@ public sealed class Apchannel : AbstractBase
double low = sourceLow[i];
// Handle NaN/Infinity
if (!double.IsFinite(high)) high = lastValidHigh;
if (!double.IsFinite(low)) low = lastValidLow;
if (!double.IsFinite(high))
{
high = lastValidHigh;
}
if (!double.IsFinite(low))
{
low = lastValidLow;
}
// Use FMA for optimal performance and precision
highEma = Math.FusedMultiplyAdd(decay, highEma, alpha * high);
@@ -346,4 +375,4 @@ public sealed class Apchannel : AbstractBase
lastValidLow = low;
}
}
}
}
+1 -1
View File
@@ -213,4 +213,4 @@ public class ApzIndicatorTests
Assert.Equal(upperDistance, lowerDistance, 6); // 6 decimal precision
}
}
}
+5 -2
View File
@@ -51,7 +51,10 @@ public sealed class ApzIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_apz == null) return;
if (_apz == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
@@ -78,4 +81,4 @@ public sealed class ApzIndicator : Indicator, IWatchlistIndicator
// Lower band
LinesSeries[2].SetValue(_apz.Lower.Value, isHot, ShowColdValues);
}
}
}
+5
View File
@@ -378,9 +378,14 @@ public sealed class ApzValidationTests : IDisposable
apzResults.Add(apz.Last.Value);
if (emaResults.Count == 0)
{
ema = bar.Close;
}
else
{
ema = alpha * bar.Close + (1 - alpha) * ema;
}
emaResults.Add(ema);
}
+82 -10
View File
@@ -116,9 +116,14 @@ public sealed class Apz : ITValuePublisher
public Apz(int period, double multiplier = 2.0)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (multiplier <= 0)
{
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
}
_period = period;
_multiplier = multiplier;
@@ -163,19 +168,31 @@ public sealed class Apz : ITValuePublisher
private (double price, double high, double low) GetValidValues(double price, double high, double low)
{
if (double.IsFinite(price))
{
_state.LastValidPrice = price;
}
else
{
price = _state.LastValidPrice;
}
if (double.IsFinite(high))
{
_state.LastValidHigh = high;
}
else
{
high = _state.LastValidHigh;
}
if (double.IsFinite(low))
{
_state.LastValidLow = low;
}
else
{
low = _state.LastValidLow;
}
return (price, high, low);
}
@@ -206,7 +223,9 @@ public sealed class Apz : ITValuePublisher
adaptiveRange *= compensator;
if (_state.E <= ConvergenceThreshold)
{
_state.IsHot = true;
}
}
double bandWidth = _multiplier * adaptiveRange;
@@ -220,9 +239,13 @@ public sealed class Apz : ITValuePublisher
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
var (price, high, low) = GetValidValues(input.Close, input.High, input.Low);
@@ -237,7 +260,10 @@ public sealed class Apz : ITValuePublisher
}
double range = high - low;
if (range < 0) range = 0; // Safety check
if (range < 0)
{
range = 0; // Safety check
}
var (middle, upper, lower) = Compute(price, range);
@@ -255,7 +281,9 @@ public sealed class Apz : ITValuePublisher
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -316,7 +344,10 @@ public sealed class Apz : ITValuePublisher
/// </summary>
public void Prime(TBarSeries source)
{
if (source.Count == 0) return;
if (source.Count == 0)
{
return;
}
// Reset state
_state = State.New();
@@ -443,15 +474,29 @@ public sealed class Apz : ITValuePublisher
{
int len = close.Length;
if (high.Length != len || low.Length != len)
{
throw new ArgumentException("Input spans must have the same length", nameof(high));
if (outputs.Middle.Length < len || outputs.Upper.Length < len || outputs.Lower.Length < len)
throw new ArgumentException("Output buffers must be at least as long as input", nameof(outputs));
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (multiplier <= 0)
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
}
if (len == 0) return;
if (outputs.Middle.Length < len || outputs.Upper.Length < len || outputs.Lower.Length < len)
{
throw new ArgumentException("Output buffers must be at least as long as input", nameof(outputs));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (multiplier <= 0)
{
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
}
if (len == 0)
{
return;
}
CalculateScalarCore(high, low, close, outputs, period, multiplier);
}
@@ -470,16 +515,29 @@ public sealed class Apz : ITValuePublisher
{
int len = close.Length;
if (high.Length != len || low.Length != len)
{
throw new ArgumentException("Input spans must have the same length", nameof(high));
}
if (outputs.Middle.Length < len || outputs.Upper.Length < len || outputs.Lower.Length < len)
{
throw new ArgumentException("Output buffers must be at least as long as input", nameof(outputs));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (multiplier <= 0)
{
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
}
if (len == 0)
{
return new ScalarState();
}
return CalculateScalarCoreWithState(high, low, close, outputs, period, multiplier);
}
@@ -540,19 +598,31 @@ public sealed class Apz : ITValuePublisher
// Get valid values
if (double.IsFinite(price))
{
state.LastValidPrice = price;
}
else
{
price = state.LastValidPrice;
}
if (double.IsFinite(h))
{
state.LastValidHigh = h;
}
else
{
h = state.LastValidHigh;
}
if (double.IsFinite(l))
{
state.LastValidLow = l;
}
else
{
l = state.LastValidLow;
}
// Handle first valid value
if (double.IsNaN(price))
@@ -585,7 +655,9 @@ public sealed class Apz : ITValuePublisher
adaptiveRange *= compensator;
if (state.E <= ConvergenceThreshold)
{
state.IsHot = true;
}
}
double bandWidth = multiplier * adaptiveRange;
@@ -626,4 +698,4 @@ public sealed class Apz : ITValuePublisher
var results = apz.Update(source);
return (results, apz);
}
}
}
@@ -189,4 +189,4 @@ public class AtrBandsIndicatorTests
indicator.Multiplier = 3.5;
Assert.Equal(3.5, indicator.Multiplier);
}
}
}
+5 -2
View File
@@ -51,7 +51,10 @@ public sealed class AtrBandsIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_atrBands == null) return;
if (_atrBands == null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
@@ -78,4 +81,4 @@ public sealed class AtrBandsIndicator : Indicator, IWatchlistIndicator
// Lower band
LinesSeries[2].SetValue(_atrBands.Lower.Value, isHot, ShowColdValues);
}
}
}
+1 -1
View File
@@ -708,4 +708,4 @@ public class AtrBandsTests
// Bands should be symmetric around middle
Assert.Equal(upperDist, lowerDist, 1e-10);
}
}
}
+117 -17
View File
@@ -114,9 +114,14 @@ public sealed class AtrBands : ITValuePublisher, IDisposable
public AtrBands(int period, double multiplier = 2.0)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (multiplier <= 0)
{
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
}
_period = period;
_multiplier = multiplier;
@@ -145,7 +150,11 @@ public sealed class AtrBands : ITValuePublisher, IDisposable
/// </summary>
public void Dispose()
{
if (_disposed) return;
if (_disposed)
{
return;
}
_disposed = true;
if (_source is not null)
@@ -173,7 +182,9 @@ public sealed class AtrBands : ITValuePublisher, IDisposable
private double CalculateTrueRange(double high, double low, double prevClose)
{
if (double.IsNaN(prevClose))
{
return high - low;
}
double hl = high - low;
double hpc = Math.Abs(high - prevClose);
@@ -188,9 +199,13 @@ public sealed class AtrBands : ITValuePublisher, IDisposable
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
// Get valid values with last-value substitution
double source = input.Close;
@@ -198,10 +213,41 @@ public sealed class AtrBands : ITValuePublisher, IDisposable
double low = input.Low;
double close = input.Close;
if (double.IsFinite(source)) _state.LastValidSource = source; else source = _state.LastValidSource;
if (double.IsFinite(high)) _state.LastValidHigh = high; else high = _state.LastValidHigh;
if (double.IsFinite(low)) _state.LastValidLow = low; else low = _state.LastValidLow;
if (double.IsFinite(close)) _state.LastValidClose = close; else close = _state.LastValidClose;
if (double.IsFinite(source))
{
_state.LastValidSource = source;
}
else
{
source = _state.LastValidSource;
}
if (double.IsFinite(high))
{
_state.LastValidHigh = high;
}
else
{
high = _state.LastValidHigh;
}
if (double.IsFinite(low))
{
_state.LastValidLow = low;
}
else
{
low = _state.LastValidLow;
}
if (double.IsFinite(close))
{
_state.LastValidClose = close;
}
else
{
close = _state.LastValidClose;
}
// Handle first valid value initialization
if (double.IsNaN(source))
@@ -248,7 +294,9 @@ public sealed class AtrBands : ITValuePublisher, IDisposable
double width = atr * _multiplier;
if (isNew)
{
_state.PrevClose = close;
}
Last = new TValue(input.Time, middle);
Upper = new TValue(input.Time, middle + width);
@@ -264,7 +312,9 @@ public sealed class AtrBands : ITValuePublisher, IDisposable
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -305,7 +355,10 @@ public sealed class AtrBands : ITValuePublisher, IDisposable
/// </summary>
public void Prime(TBarSeries source)
{
if (source.Count == 0) return;
if (source.Count == 0)
{
return;
}
// Reset state
_sourceBuffer.Clear();
@@ -325,11 +378,19 @@ public sealed class AtrBands : ITValuePublisher, IDisposable
_state.LastValidClose = bar.Close;
}
if (double.IsFinite(bar.High) && double.IsNaN(_state.LastValidHigh))
{
_state.LastValidHigh = bar.High;
}
if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow))
{
_state.LastValidLow = bar.Low;
}
if (!double.IsNaN(_state.LastValidSource) && !double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow))
{
break;
}
}
// Find valid values in warmup window if not found
@@ -396,15 +457,29 @@ public sealed class AtrBands : ITValuePublisher, IDisposable
{
int len = input.Close.Length;
if (input.High.Length != len || input.Low.Length != len)
{
throw new ArgumentException("High, Low, and Close must have the same length", nameof(input));
if (output.Middle.Length < len || output.Upper.Length < len || output.Lower.Length < len)
throw new ArgumentException("Output buffers must be at least as long as input", nameof(output));
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (multiplier <= 0)
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
}
if (len == 0) return;
if (output.Middle.Length < len || output.Upper.Length < len || output.Lower.Length < len)
{
throw new ArgumentException("Output buffers must be at least as long as input", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (multiplier <= 0)
{
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
}
if (len == 0)
{
return;
}
CalculateScalarCore(input.High, input.Low, input.Close, output.Middle, output.Upper, output.Lower, period, multiplier);
}
@@ -521,9 +596,32 @@ public sealed class AtrBands : ITValuePublisher, IDisposable
double c = close[i];
// Get valid values
if (double.IsFinite(h)) lastValidHigh = h; else h = lastValidHigh;
if (double.IsFinite(l)) lastValidLow = l; else l = lastValidLow;
if (double.IsFinite(c)) lastValidClose = c; else c = lastValidClose;
if (double.IsFinite(h))
{
lastValidHigh = h;
}
else
{
h = lastValidHigh;
}
if (double.IsFinite(l))
{
lastValidLow = l;
}
else
{
l = lastValidLow;
}
if (double.IsFinite(c))
{
lastValidClose = c;
}
else
{
c = lastValidClose;
}
if (double.IsNaN(c))
{
@@ -536,7 +634,9 @@ public sealed class AtrBands : ITValuePublisher, IDisposable
// Calculate True Range
double tr;
if (double.IsNaN(prevClose))
{
tr = h - l;
}
else
{
double hl = h - l;
@@ -591,4 +691,4 @@ public sealed class AtrBands : ITValuePublisher, IDisposable
var results = atrBands.Update(source);
return (results, atrBands);
}
}
}
@@ -184,4 +184,4 @@ public class BbandsIndicatorTests
Assert.True(double.IsFinite(series.GetValue(0)));
}
}
}
}
+1 -1
View File
@@ -72,4 +72,4 @@ public class BbandsIndicator : Indicator, IWatchlistIndicator
WidthSeries!.SetValue(bbands.Width.Value, bbands.IsHot, ShowColdValues);
PercentBSeries!.SetValue(bbands.PercentB.Value, bbands.IsHot, ShowColdValues);
}
}
}
+1 -1
View File
@@ -318,4 +318,4 @@ public class BbandsTests
Assert.Equal(batchResult[^1].Value, streamingBbands.Middle.Value, precision: 8);
Assert.Equal(middleArray[^1], streamingBbands.Middle.Value, precision: 8);
}
}
}
@@ -372,4 +372,4 @@ public sealed class BbandsValidationTests : IDisposable
}
_output.WriteLine("Bbands Batch(TSeries) validated successfully against Ooples");
}
}
}
+2 -2
View File
@@ -204,7 +204,7 @@ public sealed class Bbands : AbstractBase
{
step ??= TimeSpan.FromSeconds(1);
DateTime startTime = DateTime.UtcNow;
for (int i = 0; i < source.Length; i++)
{
Update(new TValue(startTime + i * step.Value, source[i]), isNew: true);
@@ -320,4 +320,4 @@ public sealed class Bbands : AbstractBase
lower[i] = middle[i] - offset;
}
}
}
}
@@ -42,7 +42,9 @@ public sealed class DchannelIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_indicator is null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
+35 -2
View File
@@ -42,7 +42,9 @@ public sealed class Dchannel : ITValuePublisher
public Dchannel(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
_hBuf = new double[_period];
@@ -74,14 +76,22 @@ public sealed class Dchannel : ITValuePublisher
private (double high, double low) GetValid(double high, double low)
{
if (double.IsFinite(high))
{
_state = _state with { LastValidHigh = high };
}
else
{
high = _state.LastValidHigh;
}
if (double.IsFinite(low))
{
_state = _state with { LastValidLow = low };
}
else
{
low = _state.LastValidLow;
}
return (high, low);
}
@@ -90,15 +100,21 @@ public sealed class Dchannel : ITValuePublisher
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
if (isNew)
{
_index++;
if (_count < _period)
{
_count++;
}
}
int bufIdx = (int)(_index % _period);
@@ -134,7 +150,9 @@ public sealed class Dchannel : ITValuePublisher
double mid = (top + bot) * 0.5;
if (!IsHot && _count >= _period)
{
_state = _state with { IsHot = true };
}
Last = new TValue(input.Time, mid);
Upper = new TValue(input.Time, top);
@@ -147,7 +165,9 @@ public sealed class Dchannel : ITValuePublisher
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -191,7 +211,9 @@ public sealed class Dchannel : ITValuePublisher
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
@@ -226,14 +248,25 @@ public sealed class Dchannel : ITValuePublisher
int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (high.Length != low.Length)
{
throw new ArgumentException("High and Low spans must have the same length", nameof(high));
}
if (middle.Length < high.Length || upper.Length < high.Length || lower.Length < high.Length)
{
throw new ArgumentException("Output spans must be at least as long as inputs", nameof(middle));
}
int len = high.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
double[] top = ArrayPool<double>.Shared.Rent(len);
double[] bot = ArrayPool<double>.Shared.Rent(len);
@@ -295,4 +328,4 @@ public sealed class Dchannel : ITValuePublisher
var results = indicator.Update(source);
return (results, indicator);
}
}
}
@@ -42,7 +42,9 @@ public sealed class DecaychannelIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_indicator is null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
+45 -4
View File
@@ -58,7 +58,9 @@ public sealed class Decaychannel : ITValuePublisher
public Decaychannel(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
_decayLambda = Math.Log(2.0) / period;
@@ -98,14 +100,22 @@ public sealed class Decaychannel : ITValuePublisher
private (double high, double low) GetValid(double high, double low)
{
if (double.IsFinite(high))
{
_state = _state with { LastValidHigh = high };
}
else
{
high = _state.LastValidHigh;
}
if (double.IsFinite(low))
{
_state = _state with { LastValidLow = low };
}
else
{
low = _state.LastValidLow;
}
return (high, low);
}
@@ -151,7 +161,9 @@ public sealed class Decaychannel : ITValuePublisher
{
int len = Math.Min(_count, _period);
if (len == 0)
{
return (double.NaN, double.NaN);
}
double max = double.MinValue;
double min = double.MaxValue;
@@ -159,13 +171,23 @@ public sealed class Decaychannel : ITValuePublisher
for (int i = 0; i < len; i++)
{
int idx = (int)((_index - i) % _period);
if (idx < 0) idx += _period;
if (idx < 0)
{
idx += _period;
}
double h = _hBuf[idx];
double l = _lBuf[idx];
if (h > max) max = h;
if (l < min) min = l;
if (h > max)
{
max = h;
}
if (l < min)
{
min = l;
}
}
return (max, min);
@@ -183,7 +205,9 @@ public sealed class Decaychannel : ITValuePublisher
// Now advance to new bar
_index++;
if (_count < _period)
{
_count++;
}
}
else
{
@@ -193,7 +217,9 @@ public sealed class Decaychannel : ITValuePublisher
// Re-advance to current bar position (we're reprocessing current bar)
_index++;
if (_count < _period)
{
_count++;
}
}
int bufIdx = (int)(_index % _period);
@@ -286,7 +312,9 @@ public sealed class Decaychannel : ITValuePublisher
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -329,7 +357,9 @@ public sealed class Decaychannel : ITValuePublisher
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
@@ -370,14 +400,25 @@ public sealed class Decaychannel : ITValuePublisher
int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (high.Length != low.Length)
{
throw new ArgumentException("High and Low spans must have the same length", nameof(high));
}
if (middle.Length < high.Length || upper.Length < high.Length || lower.Length < high.Length)
{
throw new ArgumentException("Output spans must be at least as long as inputs", nameof(middle));
}
int len = high.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
double decayLambda = Math.Log(2.0) / period;
+2
View File
@@ -44,7 +44,9 @@ public sealed class FcbIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_indicator is null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
+57 -1
View File
@@ -59,7 +59,9 @@ public sealed class Fcb : ITValuePublisher
public Fcb(int period = 20)
{
if (period < 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
}
_period = period;
WarmupPeriod = period + 2; // Need 2 extra bars for fractal detection
@@ -91,14 +93,22 @@ public sealed class Fcb : ITValuePublisher
private (double high, double low) GetValid(double high, double low)
{
if (double.IsFinite(high))
{
_state = _state with { LastValidHigh = high };
}
else
{
high = _state.LastValidHigh;
}
if (double.IsFinite(low))
{
_state = _state with { LastValidLow = low };
}
else
{
low = _state.LastValidLow;
}
return (high, low);
}
@@ -120,9 +130,13 @@ public sealed class Fcb : ITValuePublisher
int backIdx = (_hHead + _hCount - 1) % _period;
int bufIdx = _hDeque[backIdx] % _period;
if (_hBuf[bufIdx] <= value)
{
_hCount--;
}
else
{
break;
}
}
int tail = (_hHead + _hCount) % _period;
@@ -145,9 +159,13 @@ public sealed class Fcb : ITValuePublisher
int backIdx = (_lHead + _lCount - 1) % _period;
int bufIdx = _lDeque[backIdx] % _period;
if (_lBuf[bufIdx] >= value)
{
_lCount--;
}
else
{
break;
}
}
int tail = (_lHead + _lCount) % _period;
@@ -163,7 +181,9 @@ public sealed class Fcb : ITValuePublisher
_lCount = 0;
if (_count == 0)
{
return;
}
long startLogical = _index - _count + 1;
for (int i = 0; i < _count; i++)
@@ -199,15 +219,21 @@ public sealed class Fcb : ITValuePublisher
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
if (isNew)
{
_index++;
if (_count < _period)
{
_count++;
}
}
var (high, low) = GetValid(input.High, input.Low);
@@ -272,7 +298,9 @@ public sealed class Fcb : ITValuePublisher
double mid = (top + bot) * 0.5;
if (!_state.IsHot && _index + 1 >= WarmupPeriod)
{
_state = _state with { IsHot = true };
}
Last = new TValue(input.Time, mid);
Upper = new TValue(input.Time, top);
@@ -285,7 +313,9 @@ public sealed class Fcb : ITValuePublisher
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -329,7 +359,9 @@ public sealed class Fcb : ITValuePublisher
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
@@ -349,14 +381,25 @@ public sealed class Fcb : ITValuePublisher
int period)
{
if (period < 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
}
if (high.Length != low.Length)
{
throw new ArgumentException("High and Low spans must have the same length", nameof(high));
}
if (middle.Length < high.Length || upper.Length < high.Length || lower.Length < high.Length)
{
throw new ArgumentException("Output spans must be at least as long as inputs", nameof(middle));
}
int len = high.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
// Allocate buffers for fractal tracking and deques
double[] hBuf = ArrayPool<double>.Shared.Rent(period);
@@ -389,9 +432,14 @@ public sealed class Fcb : ITValuePublisher
if (i >= 2)
{
if (h1 > h2 && h1 > h0)
{
hiFractal = h1;
}
if (l1 < l2 && l1 < l0)
{
loFractal = l1;
}
}
int bufIdx = i % period;
@@ -410,9 +458,13 @@ public sealed class Fcb : ITValuePublisher
int backIdx = (hHead + hCount - 1) % period;
int bIdx = hDeque[backIdx] % period;
if (hBuf[bIdx] <= hiFractal)
{
hCount--;
}
else
{
break;
}
}
int tail = (hHead + hCount) % period;
hDeque[tail] = i;
@@ -429,9 +481,13 @@ public sealed class Fcb : ITValuePublisher
int backIdx = (lHead + lCount - 1) % period;
int bIdx = lDeque[backIdx] % period;
if (lBuf[bIdx] >= loFractal)
{
lCount--;
}
else
{
break;
}
}
tail = (lHead + lCount) % period;
lDeque[tail] = i;
+2
View File
@@ -49,7 +49,9 @@ public sealed class JbandsIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_indicator is null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
+2
View File
@@ -215,7 +215,9 @@ public class JbandsTests
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 123);
double[] source = new double[100];
for (int i = 0; i < source.Length; i++)
{
source[i] = gbm.Next().Close;
}
double[] middle = new double[100];
double[] upper = new double[100];
@@ -95,7 +95,9 @@ public class JbandsValidationTests
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 777);
double[] source = new double[200];
for (int i = 0; i < source.Length; i++)
{
source[i] = gbm.Next().Close;
}
double[] middle = new double[200];
double[] upper = new double[200];
+54 -5
View File
@@ -59,17 +59,28 @@ public sealed class Jbands : ITValuePublisher
public Jbands(int period, int phase = 0, double power = 0.45)
{
if (period < 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
}
if (!double.IsFinite(power))
{
throw new ArgumentException("Power must be finite.", nameof(power));
}
// Phase parameter: maps -100..100 -> 0.5..2.5
if (phase < -100)
{
_phaseParam = 0.5;
}
else if (phase > 100)
{
_phaseParam = 2.5;
}
else
{
_phaseParam = (phase * 0.01) + 1.5;
}
// Length / log / divider parameters from decompiled JMA
double lengthParam = period < 1.0000000002
@@ -129,7 +140,10 @@ public sealed class Jbands : ITValuePublisher
if (!double.IsFinite(value))
{
if (_state.Bars == 0)
{
return (double.NaN, double.NaN, double.NaN);
}
value = _state.LastPrice;
}
else
@@ -139,7 +153,9 @@ public sealed class Jbands : ITValuePublisher
_state.Bars++;
if (_state.Bars == 1)
{
return InitializeFirstBar(value);
}
return CalculateJbands(value);
}
@@ -210,8 +226,16 @@ public sealed class Jbands : ITValuePublisher
{
double ratio = Math.Max(absValue / refVolatility, 0.0);
double d = Math.Pow(ratio, _pExponent);
if (d > _logParam) d = _logParam;
if (d < 1.0) d = 1.0;
if (d > _logParam)
{
d = _logParam;
}
if (d < 1.0)
{
d = 1.0;
}
return d;
}
@@ -266,7 +290,9 @@ public sealed class Jbands : ITValuePublisher
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -319,7 +345,10 @@ public sealed class Jbands : ITValuePublisher
public void Prime(TSeries source)
{
Reset();
if (source.Count == 0) return;
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
@@ -343,13 +372,24 @@ public sealed class Jbands : ITValuePublisher
double power = 0.45)
{
if (middle.Length != source.Length)
{
throw new ArgumentException("Source and middle must have the same length.", nameof(middle));
}
if (upper.Length != source.Length)
{
throw new ArgumentException("Source and upper must have the same length.", nameof(upper));
}
if (lower.Length != source.Length)
{
throw new ArgumentException("Source and lower must have the same length.", nameof(lower));
}
if (source.Length == 0)
{
return;
}
var jbands = new Jbands(period, phase, power);
for (int i = 0; i < source.Length; i++)
@@ -366,7 +406,9 @@ public sealed class Jbands : ITValuePublisher
{
int count = _volBuffer.Count;
if (count < 16)
{
return fallback;
}
Span<double> sorted = stackalloc double[count];
_volBuffer.CopyTo(sorted);
@@ -387,8 +429,15 @@ public sealed class Jbands : ITValuePublisher
end = drop + slice - 1;
}
if (start < 0) start = 0;
if (end >= count) end = count - 1;
if (start < 0)
{
start = 0;
}
if (end >= count)
{
end = count - 1;
}
int len = end - start + 1;
return sorted.Slice(start, len).SumSIMD() / len;
@@ -48,7 +48,9 @@ public sealed class KchannelIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_indicator is null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
@@ -449,7 +449,9 @@ public sealed class KchannelValidationTests : IDisposable
// After warmup, values should be within 5% (warmup methods may differ)
if (midPct < 0.05)
{
closeCount++;
}
}
}
+44 -1
View File
@@ -52,9 +52,14 @@ public sealed class Kchannel : ITValuePublisher
public Kchannel(int period = 20, double multiplier = 2.0)
{
if (period < 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
}
if (multiplier <= 0.0)
{
throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be > 0.");
}
_period = period;
_multiplier = multiplier;
@@ -95,19 +100,31 @@ public sealed class Kchannel : ITValuePublisher
private (double close, double high, double low) GetValid(double close, double high, double low)
{
if (double.IsFinite(close))
{
_state = _state with { LastValidClose = close };
}
else
{
close = _state.LastValidClose;
}
if (double.IsFinite(high))
{
_state = _state with { LastValidHigh = high };
}
else
{
high = _state.LastValidHigh;
}
if (double.IsFinite(low))
{
_state = _state with { LastValidLow = low };
}
else
{
low = _state.LastValidLow;
}
return (close, high, low);
}
@@ -116,9 +133,13 @@ public sealed class Kchannel : ITValuePublisher
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
var (close, high, low) = GetValid(input.Close, input.High, input.Low);
@@ -144,7 +165,9 @@ public sealed class Kchannel : ITValuePublisher
}
if (isNew)
{
_state = _state with { Bars = _state.Bars + 1 };
}
// EMA with warmup compensation (sum/weight approach)
double newSum = Math.FusedMultiplyAdd(_state.EmaSum, 1.0 - _emaAlpha, close * _emaAlpha);
@@ -179,7 +202,9 @@ public sealed class Kchannel : ITValuePublisher
double lower = emaValue - width;
if (!_state.IsHot && _state.Bars >= WarmupPeriod)
{
_state = _state with { IsHot = true };
}
Last = new TValue(input.Time, emaValue);
Upper = new TValue(input.Time, upper);
@@ -192,7 +217,9 @@ public sealed class Kchannel : ITValuePublisher
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -237,7 +264,9 @@ public sealed class Kchannel : ITValuePublisher
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
@@ -259,16 +288,30 @@ public sealed class Kchannel : ITValuePublisher
double multiplier = 2.0)
{
if (period < 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
}
if (multiplier <= 0.0)
{
throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be > 0.");
}
if (high.Length != low.Length || high.Length != close.Length)
{
throw new ArgumentException("High, Low, and Close spans must have the same length", nameof(high));
}
if (middle.Length < high.Length || upper.Length < high.Length || lower.Length < high.Length)
{
throw new ArgumentException("Output spans must be at least as long as inputs", nameof(middle));
}
int len = high.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
double emaAlpha = 2.0 / (period + 1);
double atrAlpha = 1.0 / period;
+2
View File
@@ -53,7 +53,9 @@ public sealed class MaenvIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_indicator is null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
+50 -3
View File
@@ -72,9 +72,14 @@ public sealed class Maenv : ITValuePublisher
public Maenv(int period = 20, double percentage = 1.0, MaenvType maType = MaenvType.EMA)
{
if (period < 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
}
if (percentage <= 0.0)
{
throw new ArgumentOutOfRangeException(nameof(percentage), "Percentage must be > 0.");
}
_period = period;
_percentage = percentage;
@@ -142,7 +147,10 @@ public sealed class Maenv : ITValuePublisher
if (double.IsFinite(value))
{
if (isNew)
{
_state = _state with { LastValid = value };
}
return value;
}
return _state.LastValid;
@@ -159,23 +167,35 @@ public sealed class Maenv : ITValuePublisher
{
_p_state = _state;
if (_smaBuffer != null && _p_smaBuffer != null)
{
Array.Copy(_smaBuffer, _p_smaBuffer, _period);
}
if (_wmaBuffer != null && _p_wmaBuffer != null)
{
Array.Copy(_wmaBuffer, _p_wmaBuffer, _period);
}
}
else
{
_state = _p_state;
if (_smaBuffer != null && _p_smaBuffer != null)
{
Array.Copy(_p_smaBuffer, _smaBuffer, _period);
}
if (_wmaBuffer != null && _p_wmaBuffer != null)
{
Array.Copy(_p_wmaBuffer, _wmaBuffer, _period);
}
}
double value = GetValid(input.Value, isNew);
if (isNew)
{
_state = _state with { Bars = _state.Bars + 1 };
}
double middle = _maType switch
{
@@ -190,7 +210,9 @@ public sealed class Maenv : ITValuePublisher
double lower = middle - dist;
if (!_state.IsHot && _state.Bars >= WarmupPeriod)
{
_state = _state with { IsHot = true };
}
Last = new TValue(input.Time, middle);
Upper = new TValue(input.Time, upper);
@@ -203,7 +225,9 @@ public sealed class Maenv : ITValuePublisher
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -249,7 +273,10 @@ public sealed class Maenv : ITValuePublisher
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateSMA(double value, bool isNew)
{
if (_smaBuffer == null) return value;
if (_smaBuffer == null)
{
return value;
}
// Calculate new count (always increment if not full, for both isNew cases)
int currentCount = _state.SmaCount;
@@ -323,7 +350,10 @@ public sealed class Maenv : ITValuePublisher
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateWMA(double value, bool isNew)
{
if (_wmaBuffer == null) return value;
if (_wmaBuffer == null)
{
return value;
}
// Calculate count for this bar (always increment if not full, for both isNew cases)
int currentCount = _state.WmaCount;
@@ -334,13 +364,17 @@ public sealed class Maenv : ITValuePublisher
if (calcCount > 1)
{
for (int i = _period - 1; i > 0; i--)
{
_wmaBuffer[i] = _wmaBuffer[i - 1];
}
}
_wmaBuffer[0] = value;
// Persist state only for isNew=true
if (isNew)
{
_state = _state with { WmaCount = calcCount };
}
// Calculate WMA
double norm = 0.0;
@@ -361,7 +395,9 @@ public sealed class Maenv : ITValuePublisher
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
@@ -386,14 +422,25 @@ public sealed class Maenv : ITValuePublisher
MaenvType maType = MaenvType.EMA)
{
if (period < 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
}
if (percentage <= 0.0)
{
throw new ArgumentOutOfRangeException(nameof(percentage), "Percentage must be > 0.");
}
if (middle.Length < source.Length || upper.Length < source.Length || lower.Length < source.Length)
{
throw new ArgumentException("Output spans must be at least as long as input", nameof(middle));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
switch (maType)
{
@@ -41,7 +41,9 @@ public sealed class MmchannelIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_indicator is null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
+35 -2
View File
@@ -41,7 +41,9 @@ public sealed class Mmchannel : ITValuePublisher
public Mmchannel(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
_hBuf = new double[_period];
@@ -73,14 +75,22 @@ public sealed class Mmchannel : ITValuePublisher
private (double high, double low) GetValid(double high, double low)
{
if (double.IsFinite(high))
{
_state = _state with { LastValidHigh = high };
}
else
{
high = _state.LastValidHigh;
}
if (double.IsFinite(low))
{
_state = _state with { LastValidLow = low };
}
else
{
low = _state.LastValidLow;
}
return (high, low);
}
@@ -89,15 +99,21 @@ public sealed class Mmchannel : ITValuePublisher
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
if (isNew)
{
_index++;
if (_count < _period)
{
_count++;
}
}
int bufIdx = (int)(_index % _period);
@@ -132,7 +148,9 @@ public sealed class Mmchannel : ITValuePublisher
double bot = _minDeque.GetExtremum(_lBuf);
if (!IsHot && _count >= _period)
{
_state = _state with { IsHot = true };
}
// Last returns Upper by default for single-value compatibility
Last = new TValue(input.Time, top);
@@ -146,7 +164,9 @@ public sealed class Mmchannel : ITValuePublisher
public (TSeries Upper, TSeries Lower) Update(TBarSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tUpper = new List<long>(len);
@@ -184,7 +204,9 @@ public sealed class Mmchannel : ITValuePublisher
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
@@ -218,14 +240,25 @@ public sealed class Mmchannel : ITValuePublisher
int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (high.Length != low.Length)
{
throw new ArgumentException("High and Low spans must have the same length", nameof(high));
}
if (upper.Length < high.Length || lower.Length < high.Length)
{
throw new ArgumentException("Output spans must be at least as long as inputs", nameof(upper));
}
int len = high.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
Highest.Calculate(high, upper, period);
Lowest.Calculate(low, lower, period);
@@ -261,4 +294,4 @@ public sealed class Mmchannel : ITValuePublisher
var results = indicator.Update(source);
return (results, indicator);
}
}
}
@@ -42,7 +42,9 @@ public sealed class PchannelIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_indicator is null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
@@ -18,9 +18,16 @@ public sealed class PchannelValidationTests : IDisposable
private void Dispose(bool disposing)
{
if (_disposed) return;
if (_disposed)
{
return;
}
_disposed = true;
if (disposing) _testData?.Dispose();
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
+36 -1
View File
@@ -49,7 +49,9 @@ public sealed class Pchannel : ITValuePublisher
public Pchannel(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
_hBuf = new double[_period];
@@ -85,14 +87,22 @@ public sealed class Pchannel : ITValuePublisher
private (double high, double low) GetValid(double high, double low)
{
if (double.IsFinite(high))
{
_state = _state with { LastValidHigh = high };
}
else
{
high = _state.LastValidHigh;
}
if (double.IsFinite(low))
{
_state = _state with { LastValidLow = low };
}
else
{
low = _state.LastValidLow;
}
return (high, low);
}
@@ -165,7 +175,9 @@ public sealed class Pchannel : ITValuePublisher
_lCount = 0;
if (_count == 0)
{
return;
}
long startLogical = _index - _count + 1;
for (int i = 0; i < _count; i++)
@@ -183,15 +195,21 @@ public sealed class Pchannel : ITValuePublisher
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
if (isNew)
{
_index++;
if (_count < _period)
{
_count++;
}
}
int bufIdx = (int)(_index % _period);
@@ -224,7 +242,9 @@ public sealed class Pchannel : ITValuePublisher
double mid = (top + bot) * 0.5;
if (!IsHot && _count >= _period)
{
_state = _state with { IsHot = true };
}
Last = new TValue(input.Time, mid);
Upper = new TValue(input.Time, top);
@@ -237,7 +257,9 @@ public sealed class Pchannel : ITValuePublisher
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -280,7 +302,9 @@ public sealed class Pchannel : ITValuePublisher
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
@@ -317,14 +341,25 @@ public sealed class Pchannel : ITValuePublisher
int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (high.Length != low.Length)
{
throw new ArgumentException("High and Low spans must have the same length", nameof(high));
}
if (middle.Length < high.Length || upper.Length < high.Length || lower.Length < high.Length)
{
throw new ArgumentException("Output spans must be at least as long as inputs", nameof(middle));
}
int len = high.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
double[] top = ArrayPool<double>.Shared.Rent(len);
double[] bot = ArrayPool<double>.Shared.Rent(len);
@@ -50,7 +50,9 @@ public sealed class RegchannelIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_indicator is null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
+27 -1
View File
@@ -83,9 +83,14 @@ public sealed class Regchannel : ITValuePublisher
public Regchannel(int period = 20, double multiplier = 2.0)
{
if (period <= 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 1.");
}
if (multiplier <= 0)
{
throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be greater than 0.");
}
_period = period;
_multiplier = multiplier;
@@ -163,13 +168,17 @@ public sealed class Regchannel : ITValuePublisher
int head = _state.Head;
if (count < _period)
{
count++;
}
_buffer[head] = value;
int newHead = (head + 1) % _period;
if (isNew)
{
_state = _state with { Head = newHead, Count = count };
}
// Calculate linear regression and std dev of residuals
if (count <= 1)
@@ -241,7 +250,9 @@ public sealed class Regchannel : ITValuePublisher
double band = _multiplier * stdDev;
if (!_state.IsHot && count >= WarmupPeriod)
{
_state = _state with { IsHot = true };
}
_state = _state with { Slope = slope, StdDev = stdDev };
@@ -256,7 +267,9 @@ public sealed class Regchannel : ITValuePublisher
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -300,7 +313,9 @@ public sealed class Regchannel : ITValuePublisher
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
@@ -320,14 +335,25 @@ public sealed class Regchannel : ITValuePublisher
double multiplier = 2.0)
{
if (period <= 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 1.");
}
if (multiplier <= 0)
{
throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be greater than 0.");
}
if (middle.Length < source.Length || upper.Length < source.Length || lower.Length < source.Length)
{
throw new ArgumentException("Output spans must be at least as long as input", nameof(middle));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
// Precompute constants for full period
double sumXFull = 0.5 * period * (period - 1);
@@ -50,7 +50,9 @@ public sealed class SdchannelIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_indicator is null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
+27 -1
View File
@@ -83,9 +83,14 @@ public sealed class Sdchannel : ITValuePublisher
public Sdchannel(int period = 20, double multiplier = 2.0)
{
if (period <= 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 1.");
}
if (multiplier <= 0)
{
throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be greater than 0.");
}
_period = period;
_multiplier = multiplier;
@@ -163,13 +168,17 @@ public sealed class Sdchannel : ITValuePublisher
int head = _state.Head;
if (count < _period)
{
count++;
}
_buffer[head] = value;
int newHead = (head + 1) % _period;
if (isNew)
{
_state = _state with { Head = newHead, Count = count };
}
// Calculate linear regression and std dev of residuals
if (count <= 1)
@@ -241,7 +250,9 @@ public sealed class Sdchannel : ITValuePublisher
double band = _multiplier * stdDev;
if (!_state.IsHot && count >= WarmupPeriod)
{
_state = _state with { IsHot = true };
}
_state = _state with { Slope = slope, StdDev = stdDev };
@@ -256,7 +267,9 @@ public sealed class Sdchannel : ITValuePublisher
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -300,7 +313,9 @@ public sealed class Sdchannel : ITValuePublisher
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
@@ -320,14 +335,25 @@ public sealed class Sdchannel : ITValuePublisher
double multiplier = 2.0)
{
if (period <= 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 1.");
}
if (multiplier <= 0)
{
throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be greater than 0.");
}
if (middle.Length < source.Length || upper.Length < source.Length || lower.Length < source.Length)
{
throw new ArgumentException("Output spans must be at least as long as input", nameof(middle));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
// Precompute constants for full period
double sumXFull = 0.5 * period * (period - 1);
@@ -48,7 +48,9 @@ public sealed class StarchannelIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
if (_indicator is null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
bool isNew = args.IsNewBar();
+40 -1
View File
@@ -49,9 +49,14 @@ public sealed class Starchannel : ITValuePublisher
public Starchannel(int period = 20, double multiplier = 2.0)
{
if (period < 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
}
if (multiplier <= 0.0)
{
throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be > 0.");
}
_period = period;
_multiplier = multiplier;
@@ -93,19 +98,31 @@ public sealed class Starchannel : ITValuePublisher
private (double close, double high, double low) GetValid(double close, double high, double low)
{
if (double.IsFinite(close))
{
_state = _state with { LastValidClose = close };
}
else
{
close = _state.LastValidClose;
}
if (double.IsFinite(high))
{
_state = _state with { LastValidHigh = high };
}
else
{
high = _state.LastValidHigh;
}
if (double.IsFinite(low))
{
_state = _state with { LastValidLow = low };
}
else
{
low = _state.LastValidLow;
}
return (close, high, low);
}
@@ -147,7 +164,9 @@ public sealed class Starchannel : ITValuePublisher
}
if (isNew)
{
_state = _state with { Bars = _state.Bars + 1 };
}
// SMA: use RingBuffer's running sum
_smaBuffer.Add(close);
@@ -179,7 +198,9 @@ public sealed class Starchannel : ITValuePublisher
double lower = smaValue - width;
if (!_state.IsHot && _state.Bars >= WarmupPeriod)
{
_state = _state with { IsHot = true };
}
Last = new TValue(input.Time, smaValue);
Upper = new TValue(input.Time, upper);
@@ -192,7 +213,9 @@ public sealed class Starchannel : ITValuePublisher
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -237,7 +260,9 @@ public sealed class Starchannel : ITValuePublisher
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
@@ -259,16 +284,30 @@ public sealed class Starchannel : ITValuePublisher
double multiplier = 2.0)
{
if (period < 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
}
if (multiplier <= 0.0)
{
throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be > 0.");
}
if (high.Length != low.Length || high.Length != close.Length)
{
throw new ArgumentException("High, Low, and Close spans must have the same length", nameof(high));
}
if (middle.Length < high.Length || upper.Length < high.Length || lower.Length < high.Length)
{
throw new ArgumentException("Output spans must be at least as long as inputs", nameof(middle));
}
int len = high.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
double atrAlpha = 1.0 / period;
@@ -203,4 +203,4 @@ public class StbandsIndicatorTests
Assert.True(trend == 1 || trend == -1, $"Trend should be +1 or -1, got {trend}");
}
}
}
}
+1 -1
View File
@@ -70,4 +70,4 @@ public class StbandsIndicator : Indicator, IWatchlistIndicator
TrendSeries!.SetValue(stbands.Trend.Value, stbands.IsHot, ShowColdValues);
WidthSeries!.SetValue(stbands.Width.Value, stbands.IsHot, ShowColdValues);
}
}
}
+1 -1
View File
@@ -420,4 +420,4 @@ public class StbandsTests
// Assert
Assert.Equal(stbands.Upper.Value - stbands.Lower.Value, stbands.Width.Value, precision: 10);
}
}
}
@@ -376,4 +376,4 @@ public sealed class StbandsValidationTests : IDisposable
$"Higher multiplier should produce wider bands (mult={multipliers[i]})");
}
}
}
}
+11 -1
View File
@@ -175,11 +175,17 @@ public sealed class Stbands : AbstractBase
// Determine trend
if (close <= finalLower)
{
trend = 1; // Bullish
}
else if (close >= finalUpper)
{
trend = -1; // Bearish
}
else
{
trend = prevTrend;
}
}
// Update state
@@ -374,9 +380,13 @@ public sealed class Stbands : AbstractBase
// Determine trend
if (c <= finalLower)
{
currentTrend = 1;
}
else if (c >= finalUpper)
{
currentTrend = -1;
}
}
upper[i] = finalUpper;
@@ -385,4 +395,4 @@ public sealed class Stbands : AbstractBase
prevClose = c;
}
}
}
}
@@ -216,4 +216,4 @@ public class UbandsIndicatorTests
Assert.True(Math.Abs(width - (upper - lower)) < 0.0001,
$"Width ({width}) should equal Upper - Lower ({upper - lower})");
}
}
}
+1 -1
View File
@@ -68,4 +68,4 @@ public class UbandsIndicator : Indicator, IWatchlistIndicator
LowerSeries!.SetValue(ubands.Lower.Value, ubands.IsHot, ShowColdValues);
WidthSeries!.SetValue(ubands.Width.Value, ubands.IsHot, ShowColdValues);
}
}
}
+1 -1
View File
@@ -401,4 +401,4 @@ public class UbandsTests
Assert.Equal(50, result.Count);
Assert.True(double.IsFinite(result.Last.Value));
}
}
}
@@ -413,4 +413,4 @@ public sealed class UbandsValidationTests : IDisposable
Assert.True(middleVar < sourceVar, "Smoothed signal should have lower variance");
}
}
}
+1 -1
View File
@@ -385,4 +385,4 @@ public sealed class Ubands : AbstractBase
lower[i] = usf - bandOffset;
}
}
}
}
+1 -1
View File
@@ -74,4 +74,4 @@ public class UchannelIndicator : Indicator, IWatchlistIndicator
StrSeries!.SetValue(uchannel.STR.Value, uchannel.IsHot, ShowColdValues);
WidthSeries!.SetValue(uchannel.Width.Value, uchannel.IsHot, ShowColdValues);
}
}
}
+1 -1
View File
@@ -497,4 +497,4 @@ public class UchannelTests
Assert.Equal(uchannel1.Upper.Value, uchannel2.Upper.Value, precision: 10);
Assert.Equal(uchannel1.Lower.Value, uchannel2.Lower.Value, precision: 10);
}
}
}
@@ -436,4 +436,4 @@ public class UchannelValidationTests
// Should produce same results
Assert.Equal(valueBefore, valueAfter, precision: 10);
}
}
}
+15 -4
View File
@@ -87,6 +87,9 @@ public sealed class Uchannel : AbstractBase
/// <summary>Gets the channel width (Upper - Lower).</summary>
public TValue Width => new(Upper.Time, Upper.Value - Lower.Value);
/// <summary>
///
/// </summary>
/// <param name="strPeriod">Period for smoothing True Range. Must be >= 1.</param>
/// <param name="centerPeriod">Period for smoothing centerline. Must be >= 1.</param>
/// <param name="multiplier">Band multiplier for STR. Must be > 0.</param>
@@ -403,11 +406,19 @@ public sealed class Uchannel : AbstractBase
int length = close.Length;
if (high.Length != length || low.Length != length)
{
throw new ArgumentException("All input arrays must have the same length", nameof(high));
if (upper.Length != length || middle.Length != length || lower.Length != length)
throw new ArgumentException("Output arrays must match input length", nameof(upper));
}
if (length == 0) return;
if (upper.Length != length || middle.Length != length || lower.Length != length)
{
throw new ArgumentException("Output arrays must match input length", nameof(upper));
}
if (length == 0)
{
return;
}
// Compute USF coefficients
double arg_str = Math.Sqrt(2) * Math.PI / strPeriod;
@@ -503,4 +514,4 @@ public sealed class Uchannel : AbstractBase
prevClose = c;
}
}
}
}
@@ -211,7 +211,7 @@ public class VwapbandsIndicatorTests
// Same prices but different volume distributions
// Process both bars for each indicator
// Indicator1: high volume on low price, low volume on high price
indicator1.HistoricalData.AddBar(now, 100, 102, 98, 100, 10000);
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
@@ -231,4 +231,4 @@ public class VwapbandsIndicatorTests
// VWAP2 should be higher (weighted toward 110 due to high volume at high price)
Assert.True(vwap1 < vwap2, $"VWAP1 ({vwap1}) should be less than VWAP2 ({vwap2}) due to volume weighting");
}
}
}
@@ -76,4 +76,4 @@ public class VwapbandsIndicator : Indicator, IWatchlistIndicator
Lower2Series!.SetValue(vwapbands.Lower2.Value, vwapbands.IsHot, ShowColdValues);
WidthSeries!.SetValue(vwapbands.Width.Value, vwapbands.IsHot, ShowColdValues);
}
}
}
+1 -1
View File
@@ -602,4 +602,4 @@ public class VwapbandsTests
Assert.Equal(expectedVwap, vwapbands.Vwap.Value, precision: 10);
Assert.True(vwapbands.Vwap.Value < 110, "VWAP should be heavily weighted toward 100");
}
}
}
@@ -518,4 +518,4 @@ public sealed class VwapbandsValidationTests : IDisposable
_output.WriteLine("VWAPBANDS static Calculate validated successfully");
}
}
}
+1 -1
View File
@@ -414,4 +414,4 @@ public sealed class Vwapbands : AbstractBase
lower2[i] = vwapVal - 2.0 * multiplier * stdev;
}
}
}
}
@@ -258,4 +258,4 @@ public class VwapsdIndicatorTests
Assert.True(Math.Abs(width2 - 2 * width1) < 0.0001,
$"Width2 ({width2}) should be ~2x Width1 ({width1})");
}
}
}
+1 -1
View File
@@ -68,4 +68,4 @@ public class VwapsdIndicator : Indicator, IWatchlistIndicator
LowerSeries!.SetValue(vwapsd.Lower.Value, vwapsd.IsHot, ShowColdValues);
WidthSeries!.SetValue(vwapsd.Width.Value, vwapsd.IsHot, ShowColdValues);
}
}
}
+1 -1
View File
@@ -694,4 +694,4 @@ public class VwapsdTests
Assert.Equal(400.0, vwapsd.Upper.Value, precision: 10);
Assert.Equal(-100.0, vwapsd.Lower.Value, precision: 10);
}
}
}
@@ -610,4 +610,4 @@ public sealed class VwapsdValidationTests : IDisposable
_output.WriteLine("VWAPSD boundary numDevs validation completed");
}
}
}
+1 -1
View File
@@ -400,4 +400,4 @@ public sealed class Vwapsd : AbstractBase
lower[i] = vwapVal - numDevs * stdev;
}
}
}
}
+14 -1
View File
@@ -52,7 +52,9 @@ public abstract class BiInputIndicatorBase : AbstractBase
protected BiInputIndicatorBase(int period, string name)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_buffer = new RingBuffer(period);
Name = name;
@@ -176,9 +178,13 @@ public abstract class BiInputIndicatorBase : AbstractBase
double error = ComputeError(actualVal, predictedVal);
if (isNew)
{
ProcessNewBar(error);
}
else
{
ProcessBarCorrection(error);
}
double mean = _buffer.Count > 0 ? _state.Sum / _buffer.Count : error;
double result = PostProcess(mean);
@@ -248,7 +254,9 @@ public abstract class BiInputIndicatorBase : AbstractBase
BiInputBatchDelegate batchMethod)
{
if (actual.Count != predicted.Count)
{
throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted));
}
int len = actual.Count;
var t = new List<long>(len);
@@ -276,8 +284,13 @@ public abstract class BiInputIndicatorBase : AbstractBase
int period)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException("All spans must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
}
}
}
+11 -3
View File
@@ -38,7 +38,9 @@ public sealed class MonotonicDeque
public MonotonicDeque(int period)
{
if (period <= 0)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0");
}
_period = period;
_deque = new int[period];
@@ -155,7 +157,10 @@ public sealed class MonotonicDeque
public void RebuildMax(double[] buffer, long currentIndex, int count)
{
Reset();
if (count == 0) return;
if (count == 0)
{
return;
}
long startLogical = currentIndex - count + 1;
for (int i = 0; i < count; i++)
@@ -176,7 +181,10 @@ public sealed class MonotonicDeque
public void RebuildMin(double[] buffer, long currentIndex, int count)
{
Reset();
if (count == 0) return;
if (count == 0)
{
return;
}
long startLogical = currentIndex - count + 1;
for (int i = 0; i < count; i++)
@@ -186,4 +194,4 @@ public sealed class MonotonicDeque
PushMin(logicalIndex, buffer[bufIdx], buffer);
}
}
}
}
+47 -10
View File
@@ -46,7 +46,9 @@ public sealed class RingBuffer : IEnumerable<double>
public RingBuffer(int capacity)
{
if (capacity <= 0)
{
throw new ArgumentException("Capacity must be greater than 0", nameof(capacity));
}
Capacity = capacity;
_buffer = GC.AllocateArray<double>(capacity, pinned: true);
@@ -130,7 +132,11 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (_count == 0) return double.NaN;
if (_count == 0)
{
return double.NaN;
}
int idx = (_head - 1 + Capacity) % Capacity;
return _buffer[idx];
}
@@ -145,7 +151,11 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (_count == 0) return double.NaN;
if (_count == 0)
{
return double.NaN;
}
int start = _count == Capacity ? _head : 0;
return _buffer[start];
}
@@ -224,7 +234,10 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UpdateNewest(double value)
{
if (_count == 0) return;
if (_count == 0)
{
return;
}
int idx = (_head - 1 + Capacity) % Capacity;
double oldValue = _buffer[idx];
@@ -283,7 +296,10 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<double> GetSpan()
{
if (_count == 0) return ReadOnlySpan<double>.Empty;
if (_count == 0)
{
return ReadOnlySpan<double>.Empty;
}
int start = _count == Capacity ? _head : 0;
@@ -332,7 +348,11 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double Max()
{
if (_count == 0) return double.NaN;
if (_count == 0)
{
return double.NaN;
}
return MaxSimd();
}
@@ -342,7 +362,11 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double Min()
{
if (_count == 0) return double.NaN;
if (_count == 0)
{
return double.NaN;
}
return MinSimd();
}
@@ -461,7 +485,10 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double[] ToArray()
{
if (_count == 0) return Array.Empty<double>();
if (_count == 0)
{
return Array.Empty<double>();
}
double[] array = new double[_count];
CopyTo(array, 0);
@@ -474,7 +501,10 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CopyTo(double[] destination, int destinationIndex)
{
if (_count == 0) return;
if (_count == 0)
{
return;
}
int start = _count == Capacity ? _head : 0;
@@ -497,7 +527,10 @@ public sealed class RingBuffer : IEnumerable<double>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CopyTo(Span<double> destination)
{
if (_count == 0) return;
if (_count == 0)
{
return;
}
int start = _count == Capacity ? _head : 0;
@@ -533,7 +566,9 @@ public sealed class RingBuffer : IEnumerable<double>
public void CopyFrom(RingBuffer source)
{
if (source.Capacity != Capacity)
{
throw new ArgumentException("Source buffer must have same capacity", nameof(source));
}
Array.Copy(source._buffer, _buffer, Capacity);
_head = source._head;
@@ -632,7 +667,9 @@ public sealed class RingBuffer : IEnumerable<double>
public bool MoveNext()
{
if (_index + 1 >= _count)
{
return false;
}
_index++;
int bufferIdx = (_start + _index) % _buffer.Capacity;
@@ -668,4 +705,4 @@ public sealed class RingBuffer : IEnumerable<double>
public static bool operator ==(Enumerator left, Enumerator right) => left.Equals(right);
public static bool operator !=(Enumerator left, Enumerator right) => !left.Equals(right);
}
}
}
+281 -27
View File
@@ -39,11 +39,15 @@ public static class ErrorHelpers
Span<double> output)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -53,7 +57,9 @@ public static class ErrorHelpers
{
int processedCount = ComputeSignedErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
if (processedCount == len)
{
return; // All processed via SIMD
}
// Continue with scalar for remaining elements (NaN was detected)
ComputeSignedErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted);
return;
@@ -74,11 +80,15 @@ public static class ErrorHelpers
Span<double> output)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -88,7 +98,9 @@ public static class ErrorHelpers
{
int processedCount = ComputeAbsoluteErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
if (processedCount == len)
{
return; // All processed via SIMD
}
// Continue with scalar for remaining elements (NaN was detected)
ComputeAbsoluteErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted);
return;
@@ -109,11 +121,15 @@ public static class ErrorHelpers
Span<double> output)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -123,7 +139,9 @@ public static class ErrorHelpers
{
int processedCount = ComputeSquaredErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
if (processedCount == len)
{
return; // All processed via SIMD
}
// Continue with scalar for remaining elements (NaN was detected)
ComputeSquaredErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted);
return;
@@ -145,11 +163,15 @@ public static class ErrorHelpers
Span<double> output)
{
if (actual.Length != predicted.Length || actual.Length != weights.Length || actual.Length != output.Length)
{
throw new ArgumentException("All spans must have the same length", nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -165,9 +187,32 @@ public static class ErrorHelpers
double pred = predicted[i];
double wgt = weights[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(wgt)) currentValidWeight = wgt; else wgt = currentValidWeight;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
if (double.IsFinite(wgt))
{
currentValidWeight = wgt;
}
else
{
wgt = currentValidWeight;
}
double diff = act - pred;
output[i] = wgt * diff * diff;
@@ -186,11 +231,15 @@ public static class ErrorHelpers
double epsilon = 1e-10)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -227,11 +276,15 @@ public static class ErrorHelpers
double epsilon = 1e-10)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -244,8 +297,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double denominator = (Math.Abs(act) + Math.Abs(pred)) / 2.0;
output[i] = denominator < epsilon
@@ -265,11 +333,15 @@ public static class ErrorHelpers
Span<double> output)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -282,8 +354,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
// log(cosh(x)) ≈ |x| - log(2) for large |x|, numerically stable
@@ -303,11 +390,15 @@ public static class ErrorHelpers
double delta = 1.0)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -321,8 +412,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
double ratio = diff / delta;
@@ -345,11 +451,15 @@ public static class ErrorHelpers
double c = 4.685)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -363,8 +473,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
double absDiff = Math.Abs(diff);
@@ -396,11 +521,15 @@ public static class ErrorHelpers
double delta = 1.0)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -414,8 +543,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
double absDiff = Math.Abs(diff);
@@ -438,13 +582,20 @@ public static class ErrorHelpers
int resyncInterval = 1000)
{
if (errors.Length != output.Length)
{
throw new ArgumentException("Spans must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = errors.Length;
if (len == 0)
{
return;
}
double[]? rented = null;
@@ -477,7 +628,10 @@ public static class ErrorHelpers
buffer[bufferIndex] = error;
bufferIndex++;
if (bufferIndex >= period) bufferIndex = 0;
if (bufferIndex >= period)
{
bufferIndex = 0;
}
output[i] = sum / period;
@@ -486,7 +640,11 @@ public static class ErrorHelpers
{
tickCount = 0;
double recalcSum = 0;
for (int k = 0; k < period; k++) recalcSum += buffer[k];
for (int k = 0; k < period; k++)
{
recalcSum += buffer[k];
}
sum = recalcSum;
}
}
@@ -511,13 +669,20 @@ public static class ErrorHelpers
int resyncInterval = 1000)
{
if (squaredErrors.Length != output.Length)
{
throw new ArgumentException("Spans must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = squaredErrors.Length;
if (len == 0)
{
return;
}
double[]? rented = null;
@@ -550,7 +715,10 @@ public static class ErrorHelpers
buffer[bufferIndex] = sqError;
bufferIndex++;
if (bufferIndex >= period) bufferIndex = 0;
if (bufferIndex >= period)
{
bufferIndex = 0;
}
output[i] = Math.Sqrt(sum / period);
@@ -559,7 +727,11 @@ public static class ErrorHelpers
{
tickCount = 0;
double recalcSum = 0;
for (int k = 0; k < period; k++) recalcSum += buffer[k];
for (int k = 0; k < period; k++)
{
recalcSum += buffer[k];
}
sum = recalcSum;
}
}
@@ -586,13 +758,20 @@ public static class ErrorHelpers
int resyncInterval = 1000)
{
if (weightedSquaredErrors.Length != output.Length || weightedSquaredErrors.Length != weights.Length)
{
throw new ArgumentException("Spans must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = weightedSquaredErrors.Length;
if (len == 0)
{
return;
}
double[]? rentedErrors = null;
double[]? rentedWeights = null;
@@ -637,7 +816,10 @@ public static class ErrorHelpers
weightBuffer[bufferIndex] = wgt;
bufferIndex++;
if (bufferIndex >= period) bufferIndex = 0;
if (bufferIndex >= period)
{
bufferIndex = 0;
}
output[i] = sumWeights > 1e-10 ? Math.Sqrt(sumErrors / sumWeights) : 0.0;
@@ -686,11 +868,15 @@ public static class ErrorHelpers
Span<double> predictedOut)
{
if (actual.Length != predicted.Length || actual.Length != actualOut.Length || actual.Length != predictedOut.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(predictedOut));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
@@ -700,8 +886,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual;
if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted;
if (double.IsFinite(act))
{
lastValidActual = act;
}
else
{
act = lastValidActual;
}
if (double.IsFinite(pred))
{
lastValidPredicted = pred;
}
else
{
pred = lastValidPredicted;
}
actualOut[i] = act;
predictedOut[i] = pred;
@@ -717,7 +918,9 @@ public static class ErrorHelpers
for (int i = 0; i < span.Length; i++)
{
if (double.IsFinite(span[i]))
{
return span[i];
}
}
return 0.0;
}
@@ -751,14 +954,18 @@ public static class ErrorHelpers
// MoveMask returns a bitmask; all-ones means all finite (mask == 0b1111 for 4 doubles)
int mask = Avx.MoveMask(combined);
if (mask != 0b1111)
{
return false;
}
}
// Scalar tail
for (int i = vectorEnd; i < len; i++)
{
if (!double.IsFinite(actual[i]) || !double.IsFinite(predicted[i]))
{
return false;
}
}
return true;
}
@@ -767,7 +974,9 @@ public static class ErrorHelpers
for (int i = 0; i < len; i++)
{
if (!double.IsFinite(actual[i]) || !double.IsFinite(predicted[i]))
{
return false;
}
}
return true;
}
@@ -890,8 +1099,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
output[i] = act - pred;
}
@@ -1099,8 +1323,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
output[i] = Math.Abs(act - pred);
}
@@ -1156,8 +1395,23 @@ public static class ErrorHelpers
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
output[i] = diff * diff;
@@ -1181,4 +1435,4 @@ public static class ErrorHelpers
}
#endregion
}
}
+3 -1
View File
@@ -112,7 +112,9 @@ public class SimdExtensionsTests
{
double[] data = new double[1000];
for (int i = 0; i < data.Length; i++)
{
data[i] = i + 1.0;
}
var span = new ReadOnlySpan<double>(data);
const double expected = 1000.0 * 1001.0 / 2.0;
@@ -992,4 +994,4 @@ public class SimdScalarFallbackTests
Assert.Throws<ArgumentException>(() => SimdExtensions.Subtract(left, right, result));
}
}
}
+159 -25
View File
@@ -20,7 +20,9 @@ public static class SimdExtensions
for (int i = 0; i < span.Length; i++)
{
if (!double.IsFinite(span[i]))
{
return true;
}
}
return false;
}
@@ -30,7 +32,10 @@ public static class SimdExtensions
{
double scalar = 0.0;
for (int i = 0; i < span.Length; i++)
{
scalar += span[i];
}
return scalar;
}
@@ -38,13 +43,17 @@ public static class SimdExtensions
internal static double MinScalar(ReadOnlySpan<double> span)
{
if (span.Length == 0)
{
throw new ArgumentException("Span must not be empty", nameof(span));
}
double min = span[0];
for (int i = 1; i < span.Length; i++)
{
if (span[i] < min)
{
min = span[i];
}
}
return min;
}
@@ -53,13 +62,17 @@ public static class SimdExtensions
internal static double MaxScalar(ReadOnlySpan<double> span)
{
if (span.Length == 0)
{
throw new ArgumentException("Span must not be empty", nameof(span));
}
double max = span[0];
for (int i = 1; i < span.Length; i++)
{
if (span[i] > max)
{
max = span[i];
}
}
return max;
}
@@ -69,7 +82,9 @@ public static class SimdExtensions
{
// Match VarianceSIMD behavior: return 0.0 for length <= 1 to avoid divide-by-zero
if (span.Length <= 1)
{
return 0.0;
}
double sumSquares = 0.0;
for (int i = 0; i < span.Length; i++)
@@ -84,14 +99,23 @@ public static class SimdExtensions
internal static (double Min, double Max) MinMaxScalar(ReadOnlySpan<double> span)
{
if (span.Length == 0)
{
throw new ArgumentException("Span must not be empty", nameof(span));
}
double scalarMin = span[0];
double scalarMax = span[0];
for (int i = 1; i < span.Length; i++)
{
if (span[i] < scalarMin) scalarMin = span[i];
if (span[i] > scalarMax) scalarMax = span[i];
if (span[i] < scalarMin)
{
scalarMin = span[i];
}
if (span[i] > scalarMax)
{
scalarMax = span[i];
}
}
return (scalarMin, scalarMax);
}
@@ -105,7 +129,10 @@ public static class SimdExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ContainsNonFinite(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return false;
if (span.IsEmpty)
{
return false;
}
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
@@ -120,19 +147,25 @@ public static class SimdExtensions
// NaN check: NaN != NaN, so Vector.Equals(v, v) will be false for NaN lanes
var nanCheck = Vector.Equals(vector, vector);
if (!nanCheck.Equals(Vector<long>.AllBitsSet))
{
return true;
}
// Infinity check: |v| > MaxValue (Infinity has magnitude > MaxValue)
var absVec = Vector.Abs(vector);
var infCheck = Vector.GreaterThan(absVec, maxValue);
if (!infCheck.Equals(Vector<long>.Zero))
{
return true;
}
}
for (; i < span.Length; i++)
{
if (!double.IsFinite(span[i]))
{
return true;
}
}
return false;
@@ -151,7 +184,10 @@ public static class SimdExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double SumSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return 0.0;
if (span.IsEmpty)
{
return 0.0;
}
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
@@ -167,16 +203,22 @@ public static class SimdExtensions
double result = 0.0;
for (int j = 0; j < vectorSize; j++)
{
result += sum[j];
}
for (; i < span.Length; i++)
{
result += span[i];
}
// Lazy check: if result is non-finite AND input contained non-finite values, return NaN
// NaN + anything = NaN, Inf + anything finite = Inf
// If result is infinite from overflow (no input NaN/Inf), return as-is
if (!double.IsFinite(result) && span.ContainsNonFinite())
{
return double.NaN;
}
return result;
}
@@ -194,11 +236,21 @@ public static class SimdExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double MinSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return double.NaN;
if (span.Length == 1) return span[0];
if (span.IsEmpty)
{
return double.NaN;
}
if (span.Length == 1)
{
return span[0];
}
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return double.NaN;
if (span.ContainsNonFinite())
{
return double.NaN;
}
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
@@ -216,13 +268,17 @@ public static class SimdExtensions
for (int j = 1; j < vectorSize; j++)
{
if (minVec[j] < result)
{
result = minVec[j];
}
}
for (; i < span.Length; i++)
{
if (span[i] < result)
{
result = span[i];
}
}
return result;
@@ -239,11 +295,21 @@ public static class SimdExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double MaxSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return double.NaN;
if (span.Length == 1) return span[0];
if (span.IsEmpty)
{
return double.NaN;
}
if (span.Length == 1)
{
return span[0];
}
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return double.NaN;
if (span.ContainsNonFinite())
{
return double.NaN;
}
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
@@ -261,13 +327,17 @@ public static class SimdExtensions
for (int j = 1; j < vectorSize; j++)
{
if (maxVec[j] > result)
{
result = maxVec[j];
}
}
for (; i < span.Length; i++)
{
if (span[i] > result)
{
result = span[i];
}
}
return result;
@@ -284,7 +354,10 @@ public static class SimdExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double AverageSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return double.NaN;
if (span.IsEmpty)
{
return double.NaN;
}
// SumSIMD already guards against non-finite, which will propagate NaN
return span.SumSIMD() / span.Length;
}
@@ -300,13 +373,20 @@ public static class SimdExtensions
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
{
// Match VarianceScalar behavior: return 0.0 for length <= 1 to avoid inconsistency
if (span.Length <= 1) return 0.0;
if (span.Length <= 1)
{
return 0.0;
}
double m;
if (mean.HasValue)
{
// Mean provided externally - need explicit non-finite check
if (span.ContainsNonFinite()) return double.NaN;
if (span.ContainsNonFinite())
{
return double.NaN;
}
m = mean.Value;
}
else
@@ -317,7 +397,10 @@ public static class SimdExtensions
}
// If mean is NaN (from input NaN or explicit NaN mean), return NaN
if (!double.IsFinite(m)) return double.NaN;
if (!double.IsFinite(m))
{
return double.NaN;
}
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
@@ -335,7 +418,9 @@ public static class SimdExtensions
double result = 0.0;
for (int j = 0; j < vectorSize; j++)
{
result += sumSq[j];
}
for (; i < span.Length; i++)
{
@@ -368,11 +453,21 @@ public static class SimdExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static (double Min, double Max) MinMaxSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return (double.NaN, double.NaN);
if (span.Length == 1) return (span[0], span[0]);
if (span.IsEmpty)
{
return (double.NaN, double.NaN);
}
if (span.Length == 1)
{
return (span[0], span[0]);
}
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return (double.NaN, double.NaN);
if (span.ContainsNonFinite())
{
return (double.NaN, double.NaN);
}
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
@@ -392,14 +487,28 @@ public static class SimdExtensions
double max = maxVec[0];
for (int j = 1; j < vectorSize; j++)
{
if (minVec[j] < min) min = minVec[j];
if (maxVec[j] > max) max = maxVec[j];
if (minVec[j] < min)
{
min = minVec[j];
}
if (maxVec[j] > max)
{
max = maxVec[j];
}
}
for (; i < span.Length; i++)
{
if (span[i] < min) min = span[i];
if (span[i] > max) max = span[i];
if (span[i] < min)
{
min = span[i];
}
if (span[i] > max)
{
max = span[i];
}
}
return (min, max);
@@ -416,7 +525,9 @@ public static class SimdExtensions
public static void Add(ReadOnlySpan<double> left, ReadOnlySpan<double> right, Span<double> result)
{
if (left.Length != right.Length || left.Length != result.Length)
{
throw new ArgumentException("All spans must have the same length", nameof(result));
}
int i = 0;
if (Vector.IsHardwareAccelerated && left.Length >= Vector<double>.Count)
@@ -444,7 +555,9 @@ public static class SimdExtensions
public static void Scale(ReadOnlySpan<double> source, double scalar, Span<double> result)
{
if (source.Length != result.Length)
{
throw new ArgumentException("Source and result spans must have the same length", nameof(result));
}
int i = 0;
if (Vector.IsHardwareAccelerated && source.Length >= Vector<double>.Count)
@@ -472,7 +585,9 @@ public static class SimdExtensions
public static void Subtract(ReadOnlySpan<double> left, ReadOnlySpan<double> right, Span<double> result)
{
if (left.Length != right.Length || left.Length != result.Length)
{
throw new ArgumentException("All spans must have the same length", nameof(result));
}
int i = 0;
if (Vector.IsHardwareAccelerated && left.Length >= Vector<double>.Count)
@@ -500,9 +615,14 @@ public static class SimdExtensions
public static double DotProduct(this ReadOnlySpan<double> a, ReadOnlySpan<double> b)
{
if (a.Length != b.Length)
{
throw new ArgumentException("Spans must have equal length", nameof(b));
}
if (a.IsEmpty) return 0.0;
if (a.IsEmpty)
{
return 0.0;
}
int len = a.Length;
@@ -513,19 +633,33 @@ public static class SimdExtensions
ref double bRef = ref MemoryMarshal.GetReference(b);
double sum = aRef * bRef;
if (len > 1) sum += Unsafe.Add(ref aRef, 1) * Unsafe.Add(ref bRef, 1);
if (len > 2) sum += Unsafe.Add(ref aRef, 2) * Unsafe.Add(ref bRef, 2);
if (len > 1)
{
sum += Unsafe.Add(ref aRef, 1) * Unsafe.Add(ref bRef, 1);
}
if (len > 2)
{
sum += Unsafe.Add(ref aRef, 2) * Unsafe.Add(ref bRef, 2);
}
return sum;
}
if (Avx512F.IsSupported)
{
return DotProductAvx512(a, b);
}
if (Avx2.IsSupported)
{
return DotProductAvx2(a, b);
}
if (AdvSimd.Arm64.IsSupported)
{
return DotProductNeon(a, b);
}
double s1 = 0, s2 = 0, s3 = 0, s4 = 0;
ref double ar = ref MemoryMarshal.GetReference(a);
@@ -779,4 +913,4 @@ public static class SimdExtensions
return sum;
}
}
}
+1 -1
View File
@@ -508,4 +508,4 @@ public class TBarTests
// (90 + 120 + 60) / 3 = 270 / 3 = 90
Assert.Equal(90.0, bar.OHL3);
}
}
}
+1 -1
View File
@@ -53,4 +53,4 @@ public readonly record struct TBar(long Time, double Open, double High, double L
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]";
}
}
+13 -3
View File
@@ -64,7 +64,9 @@ public struct TBarSeriesEnumerator : IEnumerator<TBar>, IEquatable<TBarSeriesEnu
public bool MoveNext()
{
if (_index + 1 >= _count)
{
return false;
}
_index++;
_current = new TBar(_t[_index], _o[_index], _h[_index], _l[_index], _c[_index], _v[_index]);
@@ -323,9 +325,14 @@ public class TBarSeries : IReadOnlyList<TBar>
{
int len = t.Length;
if (o.Length != len || h.Length != len || l.Length != len || c.Length != len || v.Length != len)
{
throw new ArgumentException("All spans must have the same length", nameof(t));
}
if (len == 0) return;
if (len == 0)
{
return;
}
int oldCount = _c.Count;
int newCount = oldCount + len;
@@ -366,7 +373,10 @@ public class TBarSeries : IReadOnlyList<TBar>
public void AddRange(ReadOnlySpan<TBar> bars)
{
int len = bars.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
int oldCount = _c.Count;
int newCount = oldCount + len;
@@ -417,4 +427,4 @@ public class TBarSeries : IReadOnlyList<TBar>
IEnumerator<TBar> IEnumerable<TBar>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
+1 -1
View File
@@ -49,4 +49,4 @@ public interface ITValuePublisher
/// </summary>
event TValuePublishedHandler? Pub;
}
#pragma warning restore MA0046
#pragma warning restore MA0046
+3 -1
View File
@@ -41,7 +41,9 @@ public struct TSeriesEnumerator : IEnumerator<TValue>, IEquatable<TSeriesEnumera
public bool MoveNext()
{
if (_index + 1 >= _count)
{
return false;
}
_index++;
_current = new TValue(_t[_index], _v[_index]);
@@ -227,4 +229,4 @@ public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
+1 -1
View File
@@ -373,4 +373,4 @@ public class TValueTests
Assert.Equal(long.MaxValue, tValue.Time);
}
}
}
+25
View File
@@ -52,7 +52,9 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable
public string ToString(string? format, IFormatProvider? formatProvider)
{
if (!string.IsNullOrEmpty(format))
{
throw new NotSupportedException($"Custom format '{format}' is not supported by TValue. Use ToString() for the default format.");
}
return ToString();
}
@@ -70,7 +72,9 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable
// This is a heuristic check; actual buffer-overflow protection is performed
// by the explicit length checks that guard each write operation below.
if (destination.Length < 24)
{
return false;
}
// Write opening bracket
destination[0] = '[';
@@ -78,12 +82,18 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable
// Format datetime: yyyy-MM-dd HH:mm:ss (19 chars)
if (!AsDateTime.TryFormat(destination.Slice(pos), out int dtChars, "yyyy-MM-dd HH:mm:ss", provider))
{
return false;
}
pos += dtChars;
// Write separator
if (pos + 2 > destination.Length)
{
return false;
}
destination[pos++] = ',';
destination[pos++] = ' ';
@@ -91,20 +101,29 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable
if (double.IsPositiveInfinity(Value))
{
if (pos + 1 > destination.Length)
{
return false;
}
destination[pos++] = (char)0x221E; // 
}
else if (double.IsNegativeInfinity(Value))
{
if (pos + 2 > destination.Length)
{
return false;
}
destination[pos++] = '-';
destination[pos++] = (char)0x221E; // -
}
else if (double.IsNaN(Value))
{
if (pos + 3 > destination.Length)
{
return false;
}
destination[pos++] = 'N';
destination[pos++] = 'a';
destination[pos++] = 'N';
@@ -112,13 +131,19 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable
else
{
if (!Value.TryFormat(destination.Slice(pos), out int valueChars, "F2", provider))
{
return false;
}
pos += valueChars;
}
// Write closing bracket
if (pos + 1 > destination.Length)
{
return false;
}
destination[pos++] = ']';
charsWritten = pos;
+8 -8
View File
@@ -50,14 +50,14 @@ public sealed class StcValidationTests : IDisposable
for (int i = skip; i < qResult.Count; i++)
{
double sVal = sResult[i].Stc ?? double.NaN;
double qVal = qResult[i].Value;
double sVal = sResult[i].Stc ?? double.NaN;
double qVal = qResult[i].Value;
if (!double.IsNaN(sVal) && !double.IsNaN(qVal))
{
sumSq += (sVal - qVal) * (sVal - qVal);
count++;
}
if (!double.IsNaN(sVal) && !double.IsNaN(qVal))
{
sumSq += (sVal - qVal) * (sVal - qVal);
count++;
}
}
double rmse = Math.Sqrt(sumSq / count);
@@ -68,7 +68,7 @@ public sealed class StcValidationTests : IDisposable
Assert.True(rmse > 5.0, "QuanTAlib STC matches Skender STC, which suggests regression to Single Smoothed logic.");
// Assert values are valid
for(int i = skip; i < qResult.Count; i++)
for (int i = skip; i < qResult.Count; i++)
{
Assert.True(double.IsFinite(qResult[i].Value));
Assert.InRange(qResult[i].Value, 0, 100);
+108 -18
View File
@@ -113,7 +113,11 @@ public sealed class Stc : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double Clamp100(double x)
{
if (double.IsNaN(x)) return x;
if (double.IsNaN(x))
{
return x;
}
return Math.Clamp(x, 0, 100);
}
@@ -139,9 +143,19 @@ public sealed class Stc : AbstractBase
break;
case StcSmoothing.Digital:
if (stoch2Raw > 75) stc = 100;
else if (stoch2Raw < 25) stc = 0;
else stc = double.IsNaN(prevStc) ? stoch2Raw : prevStc;
if (stoch2Raw > 75)
{
stc = 100;
}
else if (stoch2Raw < 25)
{
stc = 0;
}
else
{
stc = double.IsNaN(prevStc) ? stoch2Raw : prevStc;
}
break;
default: // Includes StcSmoothing.None
@@ -159,26 +173,48 @@ public sealed class Stc : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool UpdateMinMaxCore(double added, double removed, bool hasRemoved, ref double min, ref double max)
{
if (double.IsNaN(added)) return false;
if (double.IsNaN(added))
{
return false;
}
bool expandMin = added < min;
bool expandMax = added > max;
if (!hasRemoved)
{
if (expandMin) min = added;
if (expandMax) max = added;
if (expandMin)
{
min = added;
}
if (expandMax)
{
max = added;
}
return false;
}
// Use relative tolerance for floating-point comparison
double tolerance = Math.Max(Math.Abs(min), Math.Abs(max)) * 1e-12;
if (tolerance < 1e-15) tolerance = 1e-15; // minimum absolute tolerance
if (tolerance < 1e-15)
{
tolerance = 1e-15; // minimum absolute tolerance
}
bool removedMin = Math.Abs(removed - min) <= tolerance;
bool removedMax = Math.Abs(removed - max) <= tolerance;
if (expandMin) min = added;
if (expandMax) max = added;
if (expandMin)
{
min = added;
}
if (expandMax)
{
max = added;
}
return (removedMin && !expandMin) || (removedMax && !expandMax);
}
@@ -193,9 +229,20 @@ public sealed class Stc : AbstractBase
max = double.NegativeInfinity;
foreach (double v in span)
{
if (double.IsNaN(v)) continue;
if (v < min) min = v;
if (v > max) max = v;
if (double.IsNaN(v))
{
continue;
}
if (v < min)
{
min = v;
}
if (v > max)
{
max = v;
}
}
}
@@ -225,8 +272,14 @@ public sealed class Stc : AbstractBase
public override TValue Update(TValue input, bool isNew = true)
{
_isNew = isNew;
if (isNew) _ps = _s;
else _s = _ps;
if (isNew)
{
_ps = _s;
}
else
{
_s = _ps;
}
var s = _s;
@@ -276,9 +329,13 @@ public sealed class Stc : AbstractBase
{
double span = s.MacdMax - s.MacdMin;
if (span > double.Epsilon)
{
stoch1Raw = 100.0 * (macd - s.MacdMin) / span;
}
else
{
stoch1Raw = double.IsNaN(s.Stoch1Ema) ? 50.0 : s.Stoch1Ema;
}
stoch1Raw = Clamp100(stoch1Raw);
}
@@ -322,9 +379,13 @@ public sealed class Stc : AbstractBase
{
double span = s.Stoch1Max - s.Stoch1Min;
if (span > double.Epsilon)
{
stoch2Raw = 100.0 * (stoch1 - s.Stoch1Min) / span;
}
else
{
stoch2Raw = double.IsNaN(s.Stoch2Ema) ? stoch1 : s.Stoch2Ema;
}
stoch2Raw = Clamp100(stoch2Raw);
}
@@ -340,7 +401,10 @@ public sealed class Stc : AbstractBase
stc = ApplySmoothing(stoch2Raw, _smoothing, _dAlpha, ref s.Stoch2Ema, ref s.PrevStc);
}
if (isNew) _samples++;
if (isNew)
{
_samples++;
}
_s = s;
Last = new TValue(input.Time, stc);
@@ -352,14 +416,19 @@ public sealed class Stc : AbstractBase
{
var result = new TSeries();
foreach (var item in source)
{
result.Add(Update(item, isNew: true));
}
return result;
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double v in source)
{
Update(new TValue(DateTime.MinValue, v), isNew: true);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -389,7 +458,9 @@ public sealed class Stc : AbstractBase
int kPeriod = 10, int dPeriod = 3, int fastLength = 23, int slowLength = 50, StcSmoothing smoothing = StcSmoothing.Ema)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output spans must be of equal length.", nameof(output));
}
double fastAlpha = 2.0 / (fastLength + 1.0);
double slowAlpha = 2.0 / (slowLength + 1.0);
@@ -465,7 +536,10 @@ public sealed class Stc : AbstractBase
double macdRemoved = macdBuf[macdIdx];
macdBuf[macdIdx] = macd;
macdIdx = (macdIdx + 1) % kPeriod;
if (!macdHasRemoved) macdCount++;
if (!macdHasRemoved)
{
macdCount++;
}
ReadOnlySpan<double> macdValidSpan = macdBuf.Slice(0, macdCount);
UpdateMinMax(macd, macdRemoved, macdHasRemoved, macdValidSpan, ref macdMin, ref macdMax);
@@ -476,9 +550,13 @@ public sealed class Stc : AbstractBase
{
double span = macdMax - macdMin;
if (span > double.Epsilon)
{
stoch1Raw = 100.0 * (macd - macdMin) / span;
}
else
{
stoch1Raw = double.IsNaN(stoch1Ema) ? 50.0 : stoch1Ema;
}
stoch1Raw = Clamp100(stoch1Raw);
}
@@ -505,7 +583,10 @@ public sealed class Stc : AbstractBase
double stochRemoved = stoch1Buf[stoch1Idx];
stoch1Buf[stoch1Idx] = stoch1;
stoch1Idx = (stoch1Idx + 1) % kPeriod;
if (!stochHasRemoved) stoch1Count++;
if (!stochHasRemoved)
{
stoch1Count++;
}
ReadOnlySpan<double> stochValidSpan = stoch1Buf.Slice(0, stoch1Count);
UpdateMinMax(stoch1, stochRemoved, stochHasRemoved, stochValidSpan, ref stoch1Min, ref stoch1Max);
@@ -517,9 +598,13 @@ public sealed class Stc : AbstractBase
{
double span = stoch1Max - stoch1Min;
if (span > double.Epsilon)
{
stoch2Raw = 100.0 * (stoch1 - stoch1Min) / span;
}
else
{
stoch2Raw = double.IsNaN(stoch2Ema) ? stoch1 : stoch2Ema;
}
stoch2Raw = Clamp100(stoch2Raw);
}
@@ -541,9 +626,14 @@ public sealed class Stc : AbstractBase
finally
{
if (rentedMacd != null)
{
ArrayPool<double>.Shared.Return(rentedMacd);
}
if (rentedStoch1 != null)
{
ArrayPool<double>.Shared.Return(rentedStoch1);
}
}
}
}
+14 -2
View File
@@ -59,7 +59,9 @@ public class AdxTests
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 50; i++)
{
adx.Update(bars[i]);
}
var originalValue = adx.Last;
@@ -110,7 +112,10 @@ public class AdxTests
for (int i = 0; i < bars.Count; i++)
{
adx.Update(bars[i]);
if (adx.IsHot) break;
if (adx.IsHot)
{
break;
}
}
Assert.True(adx.IsHot);
@@ -124,7 +129,9 @@ public class AdxTests
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 40; i++)
{
adx.Update(bars[i]);
}
var nanBar = new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 100);
var result = adx.Update(nanBar);
@@ -140,7 +147,9 @@ public class AdxTests
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 40; i++)
{
adx.Update(bars[i]);
}
var infBar = new TBar(DateTime.UtcNow, double.PositiveInfinity, double.PositiveInfinity, 0, 100, 100);
var result = adx.Update(infBar);
@@ -161,7 +170,10 @@ public class AdxTests
// 2. Streaming Mode
var streamAdx = new Adx(14);
for (int i = 0; i < bars.Count; i++)
{
streamAdx.Update(bars[i]);
}
double streamResult = streamAdx.Last.Value;
Assert.Equal(expected, streamResult, 9);
@@ -235,4 +247,4 @@ public class AdxTests
Assert.Throws<ArgumentException>(() => new Adx(0));
Assert.Throws<ArgumentException>(() => new Adx(-1));
}
}
}
+66 -16
View File
@@ -92,7 +92,9 @@ public sealed class Adx : ITValuePublisher
public Adx(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
_decay = (period - 1.0) / period;
@@ -177,14 +179,17 @@ public sealed class Adx : ITValuePublisher
double prevClose = double.IsFinite(_prevBar.Close) ? _prevBar.Close : high;
double prevHigh = double.IsFinite(_prevBar.High) ? _prevBar.High : high;
double prevLow = double.IsFinite(_prevBar.Low) ? _prevBar.Low : low;
double hl = high - low;
double hpc = Math.Abs(high - prevClose);
double lpc = Math.Abs(low - prevClose);
double tr = Math.Max(hl, Math.Max(hpc, lpc));
// Guard TR against non-finite values
if (!double.IsFinite(tr)) tr = 0;
if (!double.IsFinite(tr))
{
tr = 0;
}
// Calculate DM using guarded values
double dmPlus = 0;
@@ -193,14 +198,25 @@ public sealed class Adx : ITValuePublisher
double downMove = prevLow - low;
// Guard moves against non-finite values
if (!double.IsFinite(upMove)) upMove = 0;
if (!double.IsFinite(downMove)) downMove = 0;
if (!double.IsFinite(upMove))
{
upMove = 0;
}
if (!double.IsFinite(downMove))
{
downMove = 0;
}
if (upMove > downMove && upMove > 0)
{
dmPlus = upMove;
}
if (downMove > upMove && downMove > 0)
{
dmMinus = downMove;
}
if (isNew)
{
@@ -251,8 +267,15 @@ public sealed class Adx : ITValuePublisher
}
// Guard against NaN/Infinity in DI calculations
if (!double.IsFinite(diPlus)) diPlus = 0;
if (!double.IsFinite(diMinus)) diMinus = 0;
if (!double.IsFinite(diPlus))
{
diPlus = 0;
}
if (!double.IsFinite(diMinus))
{
diMinus = 0;
}
double diSum = diPlus + diMinus;
if (diSum > 1e-10)
@@ -261,7 +284,10 @@ public sealed class Adx : ITValuePublisher
}
// Guard against NaN/Infinity in DX calculation
if (!double.IsFinite(dx)) dx = 0;
if (!double.IsFinite(dx))
{
dx = 0;
}
// Smooth DX to get ADX
if (_dxSamples < _period)
@@ -281,17 +307,34 @@ public sealed class Adx : ITValuePublisher
}
// Final guard on ADX
if (!double.IsFinite(_adx)) _adx = _p_adx;
if (!double.IsFinite(_adx))
{
_adx = _p_adx;
}
}
// Ensure all outputs are finite; if not, use previous values or 0
if (!double.IsFinite(diPlus)) diPlus = double.IsFinite(DiPlus.Value) ? DiPlus.Value : 0;
if (!double.IsFinite(diMinus)) diMinus = double.IsFinite(DiMinus.Value) ? DiMinus.Value : 0;
if (!double.IsFinite(diPlus))
{
diPlus = double.IsFinite(DiPlus.Value) ? DiPlus.Value : 0;
}
if (!double.IsFinite(diMinus))
{
diMinus = double.IsFinite(DiMinus.Value) ? DiMinus.Value : 0;
}
// Final guard on ADX output - ensure we always return a finite value
double finalAdx = _adx;
if (!double.IsFinite(finalAdx)) finalAdx = _p_adx;
if (!double.IsFinite(finalAdx)) finalAdx = 0;
if (!double.IsFinite(finalAdx))
{
finalAdx = _p_adx;
}
if (!double.IsFinite(finalAdx))
{
finalAdx = 0;
}
DiPlus = new TValue(input.Time, diPlus);
DiMinus = new TValue(input.Time, diMinus);
@@ -309,7 +352,10 @@ public sealed class Adx : ITValuePublisher
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
var len = source.Count;
var v = new double[len];
@@ -450,7 +496,11 @@ public sealed class Adx : ITValuePublisher
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TSeries Batch(TBarSeries source, int period)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
var len = source.Count;
var v = new double[len];
Calculate(source.High.Values, source.Low.Values, source.Close.Values, period, v);
@@ -464,4 +514,4 @@ public sealed class Adx : ITValuePublisher
return new TSeries(tList, [.. v]);
}
}
}
+13 -3
View File
@@ -54,7 +54,9 @@ public sealed class Adxr : ITValuePublisher
public Adxr(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
Name = $"Adxr({period})";
@@ -126,7 +128,10 @@ public sealed class Adxr : ITValuePublisher
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var v = new double[len];
@@ -205,14 +210,19 @@ public sealed class Adxr : ITValuePublisher
finally
{
if (rentedAdx != null)
{
ArrayPool<double>.Shared.Return(rentedAdx);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TSeries Batch(TBarSeries source, int period)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var v = new double[len];
@@ -228,4 +238,4 @@ public sealed class Adxr : ITValuePublisher
return new TSeries(tList, [.. v]);
}
}
}
+2
View File
@@ -388,7 +388,9 @@ public class AmatTests
for (int i = warmup; i < source.Length; i++)
{
if (Math.Abs(tseriesResult[i].Value - trend[i]) < 0.01)
{
matched++;
}
}
// At least 95% of values after warmup should match
double matchRate = (double)matched / (source.Length - warmup);
+2 -2
View File
@@ -373,7 +373,7 @@ public sealed class AmatValidationTests : IDisposable
double trendMatchRate = (double)trendMatchCount / totalCount;
double strengthMatchRate = (double)strengthMatchCount / totalCount;
Assert.True(trendMatchRate > 0.95, $"Expected >95% trend match rate after warmup, got {trendMatchRate:P2}");
Assert.True(strengthMatchRate > 0.95, $"Expected >95% strength match rate after warmup, got {strengthMatchRate:P2}");
@@ -437,4 +437,4 @@ public sealed class AmatValidationTests : IDisposable
_output.WriteLine($"Period combination ({fastPeriod}, {slowPeriod}) validated: Trend={amat.Last.Value}, Strength={amat.Strength.Value:F2}%");
}
}
}
+51 -4
View File
@@ -123,11 +123,19 @@ public sealed class Amat : ITValuePublisher, IDisposable
public Amat(int fastPeriod = 10, int slowPeriod = 50)
{
if (fastPeriod <= 0)
{
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
}
if (slowPeriod <= 0)
{
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
}
if (fastPeriod >= slowPeriod)
{
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
}
_fastAlpha = 2.0 / (fastPeriod + 1);
_slowAlpha = 2.0 / (slowPeriod + 1);
@@ -314,7 +322,10 @@ public sealed class Amat : ITValuePublisher, IDisposable
/// <returns>Series of trend values</returns>
public TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
@@ -342,7 +353,10 @@ public sealed class Amat : ITValuePublisher, IDisposable
private static double GetCompensatedValue(double ema, double e, bool isCompensated)
{
if (isCompensated || e <= COMPENSATOR_THRESHOLD)
{
return ema;
}
return ema / (1.0 - e);
}
@@ -358,7 +372,9 @@ public sealed class Amat : ITValuePublisher, IDisposable
e *= decay;
if (!isHot && e <= COVERAGE_THRESHOLD)
{
isHot = true;
}
if (e <= COMPENSATOR_THRESHOLD)
{
@@ -391,18 +407,35 @@ public sealed class Amat : ITValuePublisher, IDisposable
int fastPeriod = 10, int slowPeriod = 50)
{
if (source.Length != trend.Length)
{
throw new ArgumentException("Source and trend must have the same length", nameof(trend));
}
if (source.Length != strength.Length)
{
throw new ArgumentException("Source and strength must have the same length", nameof(strength));
}
if (fastPeriod <= 0)
{
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
}
if (slowPeriod <= 0)
{
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
}
if (fastPeriod >= slowPeriod)
{
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
double fastAlpha = 2.0 / (fastPeriod + 1);
double slowAlpha = 2.0 / (slowPeriod + 1);
@@ -483,16 +516,30 @@ public sealed class Amat : ITValuePublisher, IDisposable
int fastPeriod = 10, int slowPeriod = 50)
{
if (source.Length != trend.Length)
{
throw new ArgumentException("Source and trend must have the same length", nameof(trend));
}
if (fastPeriod <= 0)
{
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
}
if (slowPeriod <= 0)
{
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
}
if (fastPeriod >= slowPeriod)
{
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
}
int len = source.Length;
if (len == 0) return;
if (len == 0)
{
return;
}
double fastAlpha = 2.0 / (fastPeriod + 1);
double slowAlpha = 2.0 / (slowPeriod + 1);
@@ -575,4 +622,4 @@ public sealed class Amat : ITValuePublisher, IDisposable
var amat = new Amat(fastPeriod, slowPeriod);
return amat.Update(source);
}
}
}
+11 -3
View File
@@ -67,7 +67,9 @@ public sealed class Aroon : ITValuePublisher
public Aroon(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
Name = $"Aroon({period})";
@@ -163,7 +165,10 @@ public sealed class Aroon : ITValuePublisher
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var v = new double[len];
@@ -288,7 +293,10 @@ public sealed class Aroon : ITValuePublisher
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TSeries Batch(TBarSeries source, int period)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var v = new double[len];
@@ -304,4 +312,4 @@ public sealed class Aroon : ITValuePublisher
return new TSeries(tList, [.. v]);
}
}
}
+11 -3
View File
@@ -56,7 +56,9 @@ public sealed class AroonOsc : ITValuePublisher
public AroonOsc(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
Name = $"AroonOsc({period})";
@@ -148,7 +150,10 @@ public sealed class AroonOsc : ITValuePublisher
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var v = new double[len];
@@ -189,7 +194,10 @@ public sealed class AroonOsc : ITValuePublisher
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TSeries Batch(TBarSeries source, int period)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var v = new double[len];
@@ -205,4 +213,4 @@ public sealed class AroonOsc : ITValuePublisher
return new TSeries(tList, [.. v]);
}
}
}
+9
View File
@@ -68,7 +68,9 @@ public class DmxTests
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 50; i++)
{
dmx.Update(bars[i]);
}
var originalValue = dmx.Last;
@@ -114,7 +116,9 @@ public class DmxTests
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 30; i++)
{
dmx.Update(bars[i]);
}
var nanBar = new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 100);
var result = dmx.Update(nanBar);
@@ -130,7 +134,9 @@ public class DmxTests
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 30; i++)
{
dmx.Update(bars[i]);
}
var infBar = new TBar(DateTime.UtcNow, double.PositiveInfinity, double.PositiveInfinity, 0, 100, 100);
var result = dmx.Update(infBar);
@@ -151,7 +157,10 @@ public class DmxTests
// 2. Streaming Mode
var streamDmx = new Dmx(14);
for (int i = 0; i < bars.Count; i++)
{
streamDmx.Update(bars[i]);
}
double streamResult = streamDmx.Last.Value;
Assert.Equal(expected, streamResult, 9);
+19 -1
View File
@@ -109,10 +109,14 @@ public sealed class Dmx : ITValuePublisher
double downMove = _prevBar.Low - input.Low;
if (upMove > downMove && upMove > 0)
{
dmPlusRaw = upMove;
}
if (downMove > upMove && downMove > 0)
{
dmMinusRaw = downMove;
}
double tr1 = input.High - input.Low;
double tr2 = Math.Abs(input.High - _prevBar.Close);
@@ -147,7 +151,9 @@ public sealed class Dmx : ITValuePublisher
{
int count = source.Count;
if (count == 0)
{
return [];
}
var t = new List<long>(count);
var v = new List<double>(count);
@@ -182,13 +188,19 @@ public sealed class Dmx : ITValuePublisher
{
int len = high.Length;
if (len == 0)
{
return;
}
if (low.Length != len || close.Length != len || destination.Length != len)
{
throw new ArgumentException("All input spans must have the same length", nameof(destination));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than zero.", nameof(period));
}
// Use single ArrayPool rent with slicing for better cache locality and fewer allocations
// Need 6 buffers of len each: dmPlus, dmMinus, tr, dmPlusSmooth, dmMinusSmooth, trSmooth
@@ -238,10 +250,14 @@ public sealed class Dmx : ITValuePublisher
double dmMinusRaw = 0.0;
if (upMove > downMove && upMove > 0.0)
{
dmPlusRaw = upMove;
}
if (downMove > upMove && downMove > 0.0)
{
dmMinusRaw = downMove;
}
double tr1 = h - l;
double tr2 = Math.Abs(h - pc);
@@ -275,7 +291,9 @@ public sealed class Dmx : ITValuePublisher
finally
{
if (rented != null)
{
ArrayPool<double>.Shared.Return(rented);
}
}
}
@@ -284,4 +302,4 @@ public sealed class Dmx : ITValuePublisher
var dmx = new Dmx(period);
return dmx.Update(source);
}
}
}
+1 -1
View File
@@ -265,4 +265,4 @@ public sealed class Super : ITValuePublisher
var indicator = new Super(period, multiplier);
return indicator.Update(source);
}
}
}

Some files were not shown because too many files have changed in this diff Show More