From b26d5d7751fdbdbe1f36cbcb5b8e0192b7d14058 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Wed, 10 Dec 2025 14:51:58 -0500 Subject: [PATCH] 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. --- .coderabbit.yaml | 24 +++++++++++++ .github/scanner.sh | 53 ++++++++++++++++++++++----- .github/sonarscanner.sh | 9 ++++- lib/core/simd/SimdExtensions.cs | 4 +-- lib/core/tseries/TSeries.Tests.cs | 12 +++---- lib/core/tseries/tseries.cs | 2 +- lib/feeds/csv/CsvFeed.cs | 27 +++++++------- lib/feeds/gbm/gbm.cs | 5 +-- lib/index.html | 2 +- lib/quantalib.csproj | 7 ++-- lib/trends/alma/Alma.Tests.cs | 6 ++-- lib/trends/alma/Alma.cs | 6 ++-- lib/trends/conv/Conv.Tests.cs | 55 +++++++++++++++++------------ lib/trends/conv/Conv.cs | 10 +++--- lib/trends/dema/Dema.cs | 1 + lib/trends/ema/Ema.cs | 1 + lib/trends/hma/Hma.cs | 1 + lib/trends/kama/Kama.cs | 25 +++++++++---- lib/trends/lsma/Lsma.cs | 4 ++- lib/trends/mama/Mama.Repro.Tests.cs | 16 --------- lib/trends/mama/Mama.Tests.cs | 39 +++++++++++++++++++- lib/trends/mama/Mama.cs | 39 ++------------------ lib/trends/sma/Sma.cs | 7 ++-- lib/trends/t3/T3.Tests.cs | 27 ++++++++++++++ lib/trends/t3/T3.cs | 5 +++ lib/trends/tema/Tema.cs | 7 +++- lib/trends/wma/Wma.cs | 2 +- 27 files changed, 260 insertions(+), 136 deletions(-) delete mode 100644 lib/trends/mama/Mama.Repro.Tests.cs diff --git a/.coderabbit.yaml b/.coderabbit.yaml index ff17a25d..3d242d98 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -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 diff --git a/.github/scanner.sh b/.github/scanner.sh index a31085a7..e89b24a7 100644 --- a/.github/scanner.sh +++ b/.github/scanner.sh @@ -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 diff --git a/.github/sonarscanner.sh b/.github/sonarscanner.sh index f9ce1864..e75365ac 100644 --- a/.github/sonarscanner.sh +++ b/.github/sonarscanner.sh @@ -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/" \; diff --git a/lib/core/simd/SimdExtensions.cs b/lib/core/simd/SimdExtensions.cs index 9cf53d30..efcde1ae 100644 --- a/lib/core/simd/SimdExtensions.cs +++ b/lib/core/simd/SimdExtensions.cs @@ -266,8 +266,8 @@ public static class SimdExtensions } /// - /// 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. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/lib/core/tseries/TSeries.Tests.cs b/lib/core/tseries/TSeries.Tests.cs index a6379dc6..459cb078 100644 --- a/lib/core/tseries/TSeries.Tests.cs +++ b/lib/core/tseries/TSeries.Tests.cs @@ -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(); - 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); - } } } diff --git a/lib/core/tseries/tseries.cs b/lib/core/tseries/tseries.cs index 31ad437a..72f69511 100644 --- a/lib/core/tseries/tseries.cs +++ b/lib/core/tseries/tseries.cs @@ -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 values) { diff --git a/lib/feeds/csv/CsvFeed.cs b/lib/feeds/csv/CsvFeed.cs index 3a3350b3..fcf9cfec 100644 --- a/lib/feeds/csv/CsvFeed.cs +++ b/lib/feeds/csv/CsvFeed.cs @@ -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; diff --git a/lib/feeds/gbm/gbm.cs b/lib/feeds/gbm/gbm.cs index 6b23b067..bae2b5c4 100644 --- a/lib/feeds/gbm/gbm.cs +++ b/lib/feeds/gbm/gbm.cs @@ -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; } diff --git a/lib/index.html b/lib/index.html index d82f671b..0e564699 100644 --- a/lib/index.html +++ b/lib/index.html @@ -11,7 +11,7 @@