Enhance code quality and stability across various modules

- Updated .coderabbit.yaml to exclude additional file types from reviews, improving the focus on relevant code changes.
- Modified scanner.sh to handle test failures more gracefully, ensuring that analysis stops on test failures and improving logging.
- Improved sonarscanner.sh to ensure build and test failures are properly reported, enhancing CI reliability.
- Refined SimdExtensions.cs documentation for clarity on variance calculation methods.
- Cleaned up TSeries.Tests.cs by simplifying the test structure and ensuring proper namespace usage.
- Fixed potential issues in tseries.cs by ensuring correct handling of DateTime values.
- Enhanced CsvFeed.cs to improve error handling during CSV parsing, ensuring robustness against malformed data.
- Updated GBM.cs to correctly calculate volume in the current bar, ensuring accurate simulation.
- Adjusted index.html to use globalThis for better compatibility across environments.
- Refined quantalib.csproj to exclude unnecessary files from compilation, streamlining the build process.
- Added comprehensive tests for the Mama class to ensure correct behavior during updates and state management.
- Improved error handling in various trend classes (Kama, Dema, Ema, T3, Tema, Wma) to ensure NaN values are managed correctly.
- Removed redundant Mama.Repro.Tests.cs file and consolidated tests into Mama.Tests.cs for better organization.
- Enhanced T3 and Tema classes to maintain state integrity during updates, particularly with NaN values.
This commit is contained in:
Miha Kralj
2025-12-10 14:51:58 -05:00
parent 7a4850956b
commit b26d5d7751
27 changed files with 260 additions and 136 deletions
+24
View File
@@ -8,6 +8,16 @@ reviews:
- "!**/*.csv" # Exclude CSV data files
- "!**/*.xml" # Exclude XML files
- "!**/*.json" # Exclude JSON files
- "!**/*.dll" # Exclude DLL files
- "!**/*.pdb" # Exclude PDB files
- "!**/*.sln" # Exclude Solution files
- "!**/*.csproj" # Exclude Project files
- "!**/*.ndproj" # Exclude NDepend project files
- "!**/*.user" # Exclude User files
- "!**/*.suo" # Exclude Solution User Options
- "!**/*.cache" # Exclude Cache files
- "!**/*.yml" # Exclude YAML files
- "!**/*.yaml" # Exclude YAML files
- "!**/bin/**" # Exclude bin directories
- "!**/obj/**" # Exclude obj directories
- "!**/BenchmarkDotNet.Artifacts/**" # Exclude benchmark artifacts
@@ -28,3 +38,17 @@ reviews:
- "!**/*.Quantower.Tests.cs" # Exclude Quantower adapter tests
- "!**/*.Quantower.cs" # Exclude Quantower adapters
- "!**/perf/**" # Exclude performance tests
- "!**/quantower/**" # Exclude root quantower directory
- "!**/Mocks/**" # Exclude Mocks
- "!**/test_collection_expr.cs" # Exclude scratch files
- "!**/*.so" # Exclude Shared Objects
- "!**/*.dylib" # Exclude Dynamic Libraries
- "!**/*.log" # Exclude Log files
- "!**/*.png" # Exclude PNG images
- "!**/*.jpg" # Exclude JPG images
- "!**/*.jpeg" # Exclude JPEG images
- "!**/*.gif" # Exclude GIF images
- "!**/*.props" # Exclude Build properties
- "!**/*.targets" # Exclude Build targets
- "!**/*.db" # Exclude Database files
- "!**/*.sqlite" # Exclude SQLite files
+45 -8
View File
@@ -119,8 +119,16 @@ if [ "$SKIP_BUILD" = false ]; then
dotnet build /p:DisableGitVersionTask=true /p:Version=0.0.0-wsl /p:AssemblyVersion=0.0.0.0 /p:FileVersion=0.0.0.0
log_info "Running tests with coverage..."
set +e
dotnet test --no-build --collect:"XPlat Code Coverage" \
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover,lcov || true
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover,lcov
TEST_EXIT_CODE=$?
set -e
if [ $TEST_EXIT_CODE -ne 0 ]; then
log_error "Tests failed with exit code $TEST_EXIT_CODE. Stopping analysis."
exit $TEST_EXIT_CODE
fi
# Copy coverage files to Qodana directory and convert Windows paths to Linux
log_info "Copying coverage files for Qodana..."
@@ -175,18 +183,38 @@ if [ "$SKIP_CODACY" = false ]; then
log_info "Uploading coverage to Codacy..."
# Find the most recent coverage files (one per test project)
coverage_files=$(find . -name "coverage.opencover.xml" -type f -printf '%T@ %p\n' | sort -rn | head -2 | cut -d' ' -f2-)
mapfile -t coverage_files < <(find . -name "coverage.opencover.xml" -type f -printf '%T@ %p\n' | sort -rn | head -2 | cut -d' ' -f2-)
if [ -n "$coverage_files" ]; then
for file in $coverage_files; do
if [ ${#coverage_files[@]} -gt 0 ]; then
# Download and verify Codacy reporter
CODACY_VERSION="14.1.0"
CODACY_SHA256="f1db13a9b21a9d161ddfeadf0cf6a65ffb0e9eaae8c314d3d14502946ee08475"
CODACY_URL="https://github.com/codacy/codacy-coverage-reporter/releases/download/${CODACY_VERSION}/codacy-coverage-reporter-linux"
CODACY_BIN="/tmp/codacy-coverage-reporter"
log_info "Downloading Codacy coverage reporter v${CODACY_VERSION}..."
curl -L -o "$CODACY_BIN" "$CODACY_URL"
# Verify hash
echo "$CODACY_SHA256 $CODACY_BIN" | sha256sum -c -
if [ $? -ne 0 ]; then
log_error "Codacy reporter checksum verification failed!"
rm -f "$CODACY_BIN"
exit 1
fi
chmod +x "$CODACY_BIN"
for file in "${coverage_files[@]}"; do
log_detail "Uploading: $file"
bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r "$file" --partial || true
"$CODACY_BIN" report -r "$file" --partial || true
done
# Send final notification
log_detail "Finalizing coverage report..."
bash <(curl -Ls https://coverage.codacy.com/get.sh) final || true
"$CODACY_BIN" final || true
rm -f "$CODACY_BIN"
log_success "Codacy: https://app.codacy.com/gh/mihakralj/QuanTAlib/dashboard"
else
log_warn "No coverage.opencover.xml files found"
@@ -201,12 +229,21 @@ if [ "$SKIP_QODANA" = false ]; then
log_info "Starting Qodana analysis..."
# Install dependencies required for Qodana (IntelliJ) on minimal Debian
# Note: CI environments must run as root or have passwordless sudo configured
if ! dpkg -s libfreetype6 fontconfig &> /dev/null; then
log_info "Installing missing dependencies (libfreetype6, fontconfig)..."
if [ "$EUID" -ne 0 ]; then
sudo apt-get update && sudo apt-get install -y libfreetype6 fontconfig
# Use sudo -n to fail fast if password is required
if ! sudo -n apt-get update || ! sudo -n DEBIAN_FRONTEND=noninteractive apt-get install -y libfreetype6 fontconfig; then
log_error "Dependency installation failed. Ensure passwordless sudo is configured."
exit 1
fi
else
apt-get update && apt-get install -y libfreetype6 fontconfig
if ! apt-get update || ! DEBIAN_FRONTEND=noninteractive apt-get install -y libfreetype6 fontconfig; then
log_error "Dependency installation failed."
exit 1
fi
fi
fi
+8 -1
View File
@@ -28,12 +28,19 @@ dotnet sonarscanner begin \
echo "==> Building solution..."
dotnet build --no-incremental
if [ $? -ne 0 ]; then
echo "Error: Build failed"
exit 1
fi
echo "==> Running tests with coverage..."
mkdir -p "$COVERAGE_DIR"
# Run tests with both formats if possible, or sequentially
dotnet test --no-build --collect:"XPlat Code Coverage" \
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=lcov,opencover
if [ $? -ne 0 ]; then
echo "Error: Tests failed"
exit 1
fi
# Copy coverage to Qodana directory
find . -name "coverage.info" -exec cp {} "$COVERAGE_DIR/" \;
+2 -2
View File
@@ -266,8 +266,8 @@ public static class SimdExtensions
}
/// <summary>
/// Calculates variance using SIMD vectorization (Welford's online algorithm adapted).
/// More numerically stable than naive two-pass algorithm.
/// Calculates variance using a two-pass SIMD variant that computes the mean first (via AverageSIMD) and then sums squared differences to produce variance.
/// Note that this is not the single-pass Welford algorithm.
/// Returns NaN if any input value is non-finite or if mean is non-finite.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
+4 -8
View File
@@ -1,8 +1,8 @@
namespace QuanTAlib.Tests
namespace QuanTAlib.Tests;
public class TSeriesTests
{
public class TSeriesTests
{
[Fact]
public void Constructor_Default_CreatesEmptySeries()
{
@@ -284,9 +284,8 @@ namespace QuanTAlib.Tests
series.Add(100, 1.0);
series.Add(200, 2.0);
IEnumerable enumerable = series;
var list = new List<object>();
foreach (var item in enumerable)
foreach (var item in (IEnumerable)series)
{
list.Add(item);
}
@@ -327,13 +326,10 @@ namespace QuanTAlib.Tests
{
var series = new TSeries();
Assert.Empty(series);
series.Add(100, 1.0);
Assert.Single(series);
series.Add(200, 2.0);
Assert.Equal(2, series.Count);
}
}
}
+1 -1
View File
@@ -109,7 +109,7 @@ namespace QuanTAlib;
public void Add(long time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time.Ticks, value), isNew);
public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
public void Add(IEnumerable<double> values)
{
+13 -14
View File
@@ -74,24 +74,23 @@ public class CsvFeed : IFeed
if (parts.Length != 6)
throw new FormatException($"Invalid CSV format at line {originalLineNumber}. Expected 6 columns, found {parts.Length}");
try
// Parse timestamp (YYYY-MM-DD format, assume UTC midnight)
if (!DateTime.TryParseExact(parts[0].Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var timestamp))
{
// Parse timestamp (YYYY-MM-DD format, assume UTC midnight)
var timestamp = DateTime.ParseExact(parts[0].Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
// Parse OHLCV values
double open = double.Parse(parts[1].Trim(), CultureInfo.InvariantCulture);
double high = double.Parse(parts[2].Trim(), CultureInfo.InvariantCulture);
double low = double.Parse(parts[3].Trim(), CultureInfo.InvariantCulture);
double close = double.Parse(parts[4].Trim(), CultureInfo.InvariantCulture);
double volume = double.Parse(parts[5].Trim(), CultureInfo.InvariantCulture);
series.Add(timestamp, open, high, low, close, volume, isNew: true);
throw new FormatException($"Failed to parse timestamp at line {originalLineNumber}: {line}");
}
catch (Exception ex) when (ex is FormatException or OverflowException)
// Parse OHLCV values
if (!double.TryParse(parts[1].Trim(), CultureInfo.InvariantCulture, out double open) ||
!double.TryParse(parts[2].Trim(), CultureInfo.InvariantCulture, out double high) ||
!double.TryParse(parts[3].Trim(), CultureInfo.InvariantCulture, out double low) ||
!double.TryParse(parts[4].Trim(), CultureInfo.InvariantCulture, out double close) ||
!double.TryParse(parts[5].Trim(), CultureInfo.InvariantCulture, out double volume))
{
throw new FormatException($"Failed to parse CSV line {originalLineNumber}: {line}", ex);
throw new FormatException($"Failed to parse CSV line {originalLineNumber}: {line}");
}
series.Add(timestamp, open, high, low, close, volume, isNew: true);
}
return series;
+3 -2
View File
@@ -128,14 +128,15 @@ public class GBM : IFeed
// Update current bar (intra-bar tick)
double z = NextNormal();
double price = _lastPrice * Math.Exp(_drift + _vol * z);
double volume = 1000 + _rnd.NextDouble() * 1000;
double additionalVolume = 1000 + _rnd.NextDouble() * 1000;
var bar = _currentBar;
double newClose = price;
double newHigh = Math.Max(bar.High, newClose);
double newLow = Math.Min(bar.Low, newClose);
double newVolume = bar.Volume + additionalVolume;
_currentBar = new TBar(bar.Time, bar.Open, newHigh, newLow, newClose, volume);
_currentBar = new TBar(bar.Time, bar.Open, newHigh, newLow, newClose, newVolume);
_lastPrice = newClose;
}
+1 -1
View File
@@ -11,7 +11,7 @@
<body>
<div id="app"></div>
<script>
window.$docsify = {
globalThis.$docsify = {
name: 'QuanTAlib',
repo: 'https://github.com/mihakralj/QuanTAlib',
loadSidebar: true,
+4 -3
View File
@@ -36,11 +36,12 @@
<InternalsVisibleTo Include="QuanTAlib.Tests" />
</ItemGroup>
<ItemGroup>
<Compile Include="**\*.cs" Exclude="**\*.Tests.cs;**\*.Quantower.cs;obj\**\*.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="**/*.cs" Exclude="**/*.Tests.cs;**/*.Quantower.cs;**/obj/**/*.cs;**/bin/**/*.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" Version="6.5.1">
<PrivateAssets>all</PrivateAssets>
+4 -2
View File
@@ -11,6 +11,8 @@ public class AlmaTests
{
Assert.Throws<ArgumentException>(() => new Alma(0));
Assert.Throws<ArgumentException>(() => new Alma(10, sigma: 0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Alma(10, offset: -0.1));
Assert.Throws<ArgumentOutOfRangeException>(() => new Alma(10, offset: 1.1));
var alma = new Alma(10);
Assert.NotNull(alma);
@@ -52,7 +54,7 @@ public class AlmaTests
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
series.Add(new TValue(bar.Time, bar.Close));
}
// Streaming
@@ -66,7 +68,7 @@ public class AlmaTests
var batchResults = almaBatch.Update(series);
Assert.Equal(streamingResults.Count, batchResults.Count);
for (int i = 0; i < streamingResults.Count; i++)
for (int i = 0; i < batchResults.Count; i++)
{
Assert.Equal(streamingResults[i].Value, batchResults[i].Value, 1e-9);
}
+4 -2
View File
@@ -62,6 +62,8 @@ public sealed class Alma : ITValuePublisher
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (sigma <= 0)
throw new ArgumentException("Sigma must be greater than 0", nameof(sigma));
if (offset < 0 || offset > 1)
throw new ArgumentOutOfRangeException(nameof(offset), "Offset must be between 0 and 1");
_period = period;
_offset = offset;
@@ -225,8 +227,8 @@ public sealed class Alma : ITValuePublisher
}
// Horizontal sum
vSum = Avx.Add(vSum, Avx2.Permute4x64(vSum.AsUInt64(), 0b_01_00_11_10).AsDouble());
vSum = Avx.Add(vSum, Avx2.Permute4x64(vSum.AsUInt64(), 0b_00_00_00_01).AsDouble());
vSum = Avx.Add(vSum, Avx2.Permute4x64(vSum.AsUInt64(), 0b_01_00_11_10).AsDouble()); // skipcq: CS-R1131
vSum = Avx.Add(vSum, Avx2.Permute4x64(vSum.AsUInt64(), 0b_00_00_00_01).AsDouble()); // skipcq: CS-R1131
sum = vSum.GetElement(0);
}
+32 -23
View File
@@ -23,19 +23,19 @@ public class ConvTests
// 2: 1*0.5 + 2*1.0 = 2.5
// 3: 2*0.5 + 3*1.0 = 4.0
// 4: 3*0.5 + 4*1.0 = 5.5
var kernel = new double[] { 0.5, 1.0 };
double[] kernel = [0.5, 1.0];
var conv = new Conv(kernel);
var result1 = conv.Update(new TValue(DateTime.UtcNow, 1));
Assert.Equal(1.0, result1.Value);
var result2 = conv.Update(new TValue(DateTime.UtcNow, 2));
Assert.Equal(2.5, result2.Value);
var result3 = conv.Update(new TValue(DateTime.UtcNow, 3));
Assert.Equal(4.0, result3.Value);
var result4 = conv.Update(new TValue(DateTime.UtcNow, 4));
Assert.Equal(5.5, result4.Value);
}
@@ -43,22 +43,22 @@ public class ConvTests
[Fact]
public void BarCorrection_UpdatesCorrectly()
{
var kernel = new double[] { 0.5, 1.0 };
double[] kernel = [0.5, 1.0];
var conv = new Conv(kernel);
// 1
conv.Update(new TValue(DateTime.UtcNow, 1));
// 2 (isNew=true) -> 2.5
var res1 = conv.Update(new TValue(DateTime.UtcNow, 2), isNew: true);
Assert.Equal(2.5, res1.Value);
// Update 2 to 3 (isNew=false)
// Buffer was [1, 2]. Now [1, 3].
// 1*0.5 + 3*1.0 = 3.5
var res2 = conv.Update(new TValue(DateTime.UtcNow, 3), isNew: false);
Assert.Equal(3.5, res2.Value);
// New bar 4 (isNew=true)
// Buffer was [1, 3]. New bar 4. Buffer becomes [3, 4].
// 3*0.5 + 4*1.0 = 1.5 + 4 = 5.5
@@ -69,16 +69,16 @@ public class ConvTests
[Fact]
public void NanHandling_UsesLastValid()
{
var kernel = new double[] { 1.0, 1.0 }; // Sum of last 2
double[] kernel = [1.0, 1.0]; // Sum of last 2
var conv = new Conv(kernel);
// 1 -> 1
conv.Update(new TValue(DateTime.UtcNow, 1));
// NaN -> treated as 1. Buffer: [1, 1]. Result: 2.
var res = conv.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.Equal(2.0, res.Value);
// 2 -> Buffer: [1, 2]. Result: 3.
res = conv.Update(new TValue(DateTime.UtcNow, 2));
Assert.Equal(3.0, res.Value);
@@ -87,37 +87,46 @@ public class ConvTests
[Fact]
public void StaticCalculate_MatchesObjectApi()
{
var kernel = new double[] { 0.5, 1.0 };
double[] kernel = [0.5, 1.0];
var source = new TSeries();
source.Add(new TValue(DateTime.UtcNow, 1));
source.Add(new TValue(DateTime.UtcNow, 2));
source.Add(new TValue(DateTime.UtcNow, 3));
source.Add(new TValue(DateTime.UtcNow, 4));
var result = Conv.Calculate(source, kernel);
Assert.Equal(1.0, result.Values[0]);
Assert.Equal(2.5, result.Values[1]);
Assert.Equal(4.0, result.Values[2]);
Assert.Equal(5.5, result.Values[3]);
}
[Fact]
public void Reset_ClearsState()
{
var kernel = new double[] { 1.0, 1.0 };
double[] kernel = [1.0, 1.0];
var conv = new Conv(kernel);
conv.Update(new TValue(DateTime.UtcNow, 1));
conv.Update(new TValue(DateTime.UtcNow, 2));
Assert.True(conv.IsHot);
conv.Reset();
Assert.False(conv.IsHot);
Assert.Equal(0, conv.Last.Value);
// Should behave as new
var res = conv.Update(new TValue(DateTime.UtcNow, 1));
Assert.Equal(1.0, res.Value);
}
[Fact]
public void LeadingNaN_RemainsNaN()
{
double[] kernel = [1.0];
var conv = new Conv(kernel);
var res = conv.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsNaN(res.Value));
}
}
+6 -4
View File
@@ -49,6 +49,8 @@ public sealed class Conv : ITValuePublisher
Array.Copy(kernel, _kernel, _period);
_buffer = new RingBuffer(_period);
Name = $"Conv({_period})";
_lastValidValue = double.NaN;
_p_lastValidValue = double.NaN;
}
public Conv(ITValuePublisher source, double[] kernel) : this(kernel)
@@ -154,7 +156,7 @@ public sealed class Conv : ITValuePublisher
}
else
{
_lastValidValue = 0;
_lastValidValue = double.NaN;
}
_buffer.Clear();
@@ -516,7 +518,7 @@ public sealed class Conv : ITValuePublisher
// Use stackalloc for small kernels to avoid heap allocation
Span<double> window = period <= 256 ? stackalloc double[period] : new double[period];
double lastValid = 0;
double lastValid = double.NaN;
int windowIdx = 0; // Points to where the NEXT value goes (circular)
int count = 0;
@@ -563,8 +565,8 @@ public sealed class Conv : ITValuePublisher
public void Reset()
{
_buffer.Clear();
_lastValidValue = 0;
_p_lastValidValue = 0;
_lastValidValue = double.NaN;
_p_lastValidValue = double.NaN;
_head = 0;
Last = default;
}
+1
View File
@@ -164,6 +164,7 @@ public sealed class Dema : ITValuePublisher
_p_state1 = s1;
_p_state2 = s2;
_lastValidValue = lastValid;
_p_lastValidValue = lastValid;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
+1
View File
@@ -176,6 +176,7 @@ public sealed class Ema : ITValuePublisher
sourceTimes.CopyTo(tSpan);
_p_state = _state;
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
+1
View File
@@ -96,6 +96,7 @@ public sealed class Hma : ITValuePublisher
int lookback = _period + (int)Math.Sqrt(_period) + 10; // Sufficient lookback
int startIndex = Math.Max(0, len - lookback);
_sampleCount = startIndex;
for (int i = startIndex; i < len; i++)
{
+19 -6
View File
@@ -75,6 +75,7 @@ public sealed class Kama : ITValuePublisher
Name = $"Kama({period}, {fastPeriod}, {slowPeriod})";
_kama = double.NaN;
_lastValidValue = double.NaN;
}
public Kama(ITValuePublisher source, int period = 10, int fastPeriod = 2, int slowPeriod = 30)
@@ -98,6 +99,12 @@ public sealed class Kama : ITValuePublisher
public TValue Update(TValue input, bool isNew = true)
{
double val = GetValidValue(input.Value);
if (double.IsNaN(val))
{
Last = new TValue(input.Time, double.NaN);
Pub?.Invoke(Last);
return Last;
}
if (isNew)
{
@@ -190,11 +197,11 @@ public sealed class Kama : ITValuePublisher
v.Add(outputSpan[i]);
}
// Restore state by replaying last few bars
// This is expensive but necessary to sync the object state
// Restore state by replaying the entire series
// This is expensive but necessary to sync the object state correctly
// because KAMA is recursive (IIR) and depends on the full history.
Reset();
int startIndex = Math.Max(0, len - _period - 1);
for (int i = startIndex; i < len; i++)
for (int i = 0; i < len; i++)
{
Update(source[i]);
}
@@ -220,7 +227,7 @@ public sealed class Kama : ITValuePublisher
double volatilitySum = 0;
double kama = 0;
bool kamaInitialized = false;
double lastValid = 0;
double lastValid = double.NaN;
for (int i = 0; i < source.Length; i++)
{
@@ -230,6 +237,12 @@ public sealed class Kama : ITValuePublisher
else
val = lastValid;
if (double.IsNaN(val))
{
output[i] = double.NaN;
continue;
}
// Add to buffer
double removed = buffer[bufferIdx];
buffer[bufferIdx] = val;
@@ -298,7 +311,7 @@ public sealed class Kama : ITValuePublisher
_volatilitySum = 0;
_p_volatilitySum = 0;
_lastDiffOut = 0;
_lastValidValue = 0;
_lastValidValue = double.NaN;
Last = default;
}
}
+3 -1
View File
@@ -271,15 +271,17 @@ public sealed class Lsma : ITValuePublisher
_lastValidValue = 0;
}
double lastProcessedValue = _lastValidValue;
for (int i = startIndex; i < len; i++)
{
double val = GetValidValue(source.Values[i]);
UpdateState(val);
lastProcessedValue = val;
}
_p_sum_y = _sum_y;
_p_sum_xy = _sum_xy;
_p_last_val = source.Values[len - 1];
_p_last_val = lastProcessedValue;
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
-16
View File
@@ -1,16 +0,0 @@
namespace QuanTAlib.Tests;
public class MamaReproTests
{
[Fact]
public void Constructor_ThrowsArgumentException_WhenSlowLimitIsZero()
{
Assert.Throws<ArgumentException>(() => new Mama(0.5, 0.0));
}
[Fact]
public void Constructor_ThrowsArgumentException_WhenSlowLimitIsNegative()
{
Assert.Throws<ArgumentException>(() => new Mama(0.5, -0.1));
}
}
+38 -1
View File
@@ -33,7 +33,8 @@ public class MamaTests
var result = mama.Update(input);
Assert.True(double.IsNaN(result.Value));
// Should return 0.0 (last valid price default) instead of NaN to avoid state corruption
Assert.Equal(0.0, result.Value);
}
[Fact]
@@ -62,4 +63,40 @@ public class MamaTests
Assert.True(eventFired);
}
[Fact]
public void Update_Series_AppendsData()
{
var mama1 = new Mama();
var mama2 = new Mama();
var data = new TSeries();
var now = DateTime.UtcNow;
for (int i = 0; i < 50; i++)
{
data.Add(new TValue(now.AddMinutes(i), 100.0 + Math.Sin(i * 0.1) * 10));
}
// Case 1: Update all at once
var result1 = mama1.Update(data);
// Case 2: Update in chunks
var chunk1 = new TSeries();
var chunk2 = new TSeries();
for (int i = 0; i < 25; i++) chunk1.Add(data[i]);
for (int i = 25; i < 50; i++) chunk2.Add(data[i]);
mama2.Update(chunk1);
var result2 = mama2.Update(chunk2);
// Verify final state is same
Assert.Equal(mama1.Last.Value, mama2.Last.Value, 6);
Assert.Equal(mama1.Fama.Value, mama2.Fama.Value, 6);
// Verify the returned series from the second chunk matches the second half of the full result
for (int i = 0; i < 25; i++)
{
Assert.Equal(result1[25 + i].Value, result2[i].Value, 6);
}
}
}
+3 -36
View File
@@ -204,8 +204,8 @@ public sealed class Mama : ITValuePublisher
else
{
// Initialization phase
_sumPr += input.Value;
double avg = _index > 0 ? _sumPr / _index : input.Value;
_sumPr += price;
double avg = _index > 0 ? _sumPr / _index : price;
_mama = avg;
_fama = avg;
@@ -230,47 +230,14 @@ public sealed class Mama : ITValuePublisher
var v = new List<double>(len);
var t = new List<long>(len);
var temp = new Mama(_fastLimit, _slowLimit);
for (int i = 0; i < len; i++)
{
var item = source[i];
var result = temp.Update(item);
var result = Update(item);
v.Add(result.Value);
t.Add(item.Time);
}
// Copy state from temp to this
_period = temp._period;
_p_period = temp._p_period;
_phase = temp._phase;
_p_phase = temp._p_phase;
_mama = temp._mama;
_p_mama = temp._p_mama;
_fama = temp._fama;
_p_fama = temp._p_fama;
_sumPr = temp._sumPr;
_p_sumPr = temp._p_sumPr;
_index = temp._index;
_i2 = temp._i2;
_p_i2 = temp._p_i2;
_q2 = temp._q2;
_p_q2 = temp._p_q2;
_re = temp._re;
_p_re = temp._p_re;
_im = temp._im;
_p_im = temp._p_im;
_lastValidPrice = temp._lastValidPrice;
_priceBuffer.CopyFrom(temp._priceBuffer);
_smoothBuffer.CopyFrom(temp._smoothBuffer);
_detrender.CopyFrom(temp._detrender);
_I1_buffer.CopyFrom(temp._I1_buffer);
_Q1_buffer.CopyFrom(temp._Q1_buffer);
Last = temp.Last;
Fama = temp.Fama;
return new TSeries(t, v);
}
+4 -3
View File
@@ -157,6 +157,7 @@ public sealed class Sma : ITValuePublisher
if (startIndex > 0)
{
_lastValidValue = 0;
for (int i = startIndex - 1; i >= 0; i--)
{
if (double.IsFinite(source.Values[i]))
@@ -182,7 +183,7 @@ public sealed class Sma : ITValuePublisher
}
_p_sum = _sum;
_p_lastInput = source.Values[len - 1];
_p_lastInput = GetValidValue(source.Values[len - 1]);
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
@@ -329,11 +330,11 @@ public sealed class Sma : ITValuePublisher
var vDelta = Avx.Subtract(vNew, vOld);
var vShift1 = Avx2.Permute4x64(vDelta.AsUInt64(), 0b_10_01_00_00).AsDouble();
var vShift1 = Avx2.Permute4x64(vDelta.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShift1 = Avx.Blend(vZero, vShift1, 0b_1110);
var vP1 = Avx.Add(vDelta, vShift1);
var vShift2 = Avx2.Permute4x64(vP1.AsUInt64(), 0b_01_00_00_00).AsDouble();
var vShift2 = Avx2.Permute4x64(vP1.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShift2 = Avx.Blend(vZero, vShift2, 0b_1100);
var vP2 = Avx.Add(vP1, vShift2);
+27
View File
@@ -1,3 +1,5 @@
using Xunit;
using System;
namespace QuanTAlib.Tests;
@@ -101,4 +103,29 @@ public class T3Tests
// Check last values match
Assert.Equal(resSeries.Last.Value, resSpan[count-1], 1e-9);
}
[Fact]
public void T3_BarCorrection_WithNaN_RestoresPreviousValidValue()
{
var t3 = new T3(10);
var time = DateTime.UtcNow;
// Step 1: Update with valid value
t3.Update(new TValue(time, 100), isNew: true);
// Step 2: Update with another valid value
t3.Update(new TValue(time.AddMinutes(1), 200), isNew: true);
double valAfter200 = t3.Last.Value;
// Step 3: Correct with NaN (should use 100)
t3.Update(new TValue(time.AddMinutes(1), double.NaN), isNew: false);
double valAfterNaN = t3.Last.Value;
// Step 4: Correct with 100 (should match NaN result)
t3.Update(new TValue(time.AddMinutes(1), 100), isNew: false);
double valAfter100 = t3.Last.Value;
Assert.NotEqual(valAfter200, valAfterNaN); // Should not be the same as 200
Assert.Equal(valAfter100, valAfterNaN, 1e-9); // Should be the same as using 100
}
}
+5
View File
@@ -83,6 +83,7 @@ public sealed class T3 : ITValuePublisher
private State _state = State.New();
private State _p_state = State.New();
private double _lastValidValue;
private double _p_lastValidValue;
/// <summary>
/// Display name for the indicator.
@@ -157,10 +158,12 @@ public sealed class T3 : ITValuePublisher
if (isNew)
{
_p_state = _state;
_p_lastValidValue = _lastValidValue;
}
else
{
_state = _p_state;
_lastValidValue = _p_lastValidValue;
}
double val = GetValidValue(input.Value);
@@ -196,6 +199,7 @@ public sealed class T3 : ITValuePublisher
sourceTimes.CopyTo(tSpan);
_p_state = _state;
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
@@ -283,6 +287,7 @@ public sealed class T3 : ITValuePublisher
_state = State.New();
_p_state = _state;
_lastValidValue = 0;
_p_lastValidValue = 0;
Last = default;
}
}
+6 -1
View File
@@ -61,6 +61,7 @@ public sealed class Tema : ITValuePublisher
private EmaState _p_state3 = EmaState.New();
private double _lastValidValue;
private double _p_lastValidValue;
public string Name { get; }
public TValue Last { get; private set; }
@@ -83,7 +84,7 @@ public sealed class Tema : ITValuePublisher
public Tema(double alpha)
{
if (alpha <= 0 || alpha > 1) throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
if (alpha <= 0 || alpha >= 1) throw new ArgumentException("Alpha must be strictly between 0 and 1", nameof(alpha));
_alpha = alpha;
_decay = 1.0 - alpha;
@@ -98,12 +99,14 @@ public sealed class Tema : ITValuePublisher
_p_state1 = _state1;
_p_state2 = _state2;
_p_state3 = _state3;
_p_lastValidValue = _lastValidValue;
}
else
{
_state1 = _p_state1;
_state2 = _p_state2;
_state3 = _p_state3;
_lastValidValue = _p_lastValidValue;
}
// EMA1
@@ -174,6 +177,7 @@ public sealed class Tema : ITValuePublisher
_p_state2 = s2;
_p_state3 = s3;
_lastValidValue = lastValid;
_p_lastValidValue = lastValid;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
@@ -343,6 +347,7 @@ public sealed class Tema : ITValuePublisher
_p_state2 = EmaState.New();
_p_state3 = EmaState.New();
_lastValidValue = 0;
_p_lastValidValue = 0;
Last = default;
}
}
+1 -1
View File
@@ -188,7 +188,7 @@ public sealed class Wma : ITValuePublisher
_p_sum = _sum;
_p_wsum = _wsum;
_p_lastInput = _lastValidValue;
_p_lastInput = GetValidValue(source.Values[len - 1]);
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);