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 - "!**/*.csv" # Exclude CSV data files
- "!**/*.xml" # Exclude XML files - "!**/*.xml" # Exclude XML files
- "!**/*.json" # Exclude JSON 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 - "!**/bin/**" # Exclude bin directories
- "!**/obj/**" # Exclude obj directories - "!**/obj/**" # Exclude obj directories
- "!**/BenchmarkDotNet.Artifacts/**" # Exclude benchmark artifacts - "!**/BenchmarkDotNet.Artifacts/**" # Exclude benchmark artifacts
@@ -28,3 +38,17 @@ reviews:
- "!**/*.Quantower.Tests.cs" # Exclude Quantower adapter tests - "!**/*.Quantower.Tests.cs" # Exclude Quantower adapter tests
- "!**/*.Quantower.cs" # Exclude Quantower adapters - "!**/*.Quantower.cs" # Exclude Quantower adapters
- "!**/perf/**" # Exclude performance tests - "!**/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 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..." log_info "Running tests with coverage..."
set +e
dotnet test --no-build --collect:"XPlat Code Coverage" \ 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 # Copy coverage files to Qodana directory and convert Windows paths to Linux
log_info "Copying coverage files for Qodana..." log_info "Copying coverage files for Qodana..."
@@ -175,18 +183,38 @@ if [ "$SKIP_CODACY" = false ]; then
log_info "Uploading coverage to Codacy..." log_info "Uploading coverage to Codacy..."
# Find the most recent coverage files (one per test project) # 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 if [ ${#coverage_files[@]} -gt 0 ]; then
for file in $coverage_files; do # 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" 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 done
# Send final notification # Send final notification
log_detail "Finalizing coverage report..." 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" log_success "Codacy: https://app.codacy.com/gh/mihakralj/QuanTAlib/dashboard"
else else
log_warn "No coverage.opencover.xml files found" log_warn "No coverage.opencover.xml files found"
@@ -201,12 +229,21 @@ if [ "$SKIP_QODANA" = false ]; then
log_info "Starting Qodana analysis..." log_info "Starting Qodana analysis..."
# Install dependencies required for Qodana (IntelliJ) on minimal Debian # 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 if ! dpkg -s libfreetype6 fontconfig &> /dev/null; then
log_info "Installing missing dependencies (libfreetype6, fontconfig)..." log_info "Installing missing dependencies (libfreetype6, fontconfig)..."
if [ "$EUID" -ne 0 ]; then 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 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
fi fi
+8 -1
View File
@@ -28,12 +28,19 @@ dotnet sonarscanner begin \
echo "==> Building solution..." echo "==> Building solution..."
dotnet build --no-incremental dotnet build --no-incremental
if [ $? -ne 0 ]; then
echo "Error: Build failed"
exit 1
fi
echo "==> Running tests with coverage..." echo "==> Running tests with coverage..."
mkdir -p "$COVERAGE_DIR" mkdir -p "$COVERAGE_DIR"
# Run tests with both formats if possible, or sequentially
dotnet test --no-build --collect:"XPlat Code Coverage" \ dotnet test --no-build --collect:"XPlat Code Coverage" \
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=lcov,opencover -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=lcov,opencover
if [ $? -ne 0 ]; then
echo "Error: Tests failed"
exit 1
fi
# Copy coverage to Qodana directory # Copy coverage to Qodana directory
find . -name "coverage.info" -exec cp {} "$COVERAGE_DIR/" \; find . -name "coverage.info" -exec cp {} "$COVERAGE_DIR/" \;
+2 -2
View File
@@ -266,8 +266,8 @@ public static class SimdExtensions
} }
/// <summary> /// <summary>
/// Calculates variance using SIMD vectorization (Welford's online algorithm adapted). /// Calculates variance using a two-pass SIMD variant that computes the mean first (via AverageSIMD) and then sums squared differences to produce variance.
/// More numerically stable than naive two-pass algorithm. /// 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. /// Returns NaN if any input value is non-finite or if mean is non-finite.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
+4 -8
View File
@@ -1,8 +1,8 @@
namespace QuanTAlib.Tests namespace QuanTAlib.Tests;
public class TSeriesTests
{ {
public class TSeriesTests
{
[Fact] [Fact]
public void Constructor_Default_CreatesEmptySeries() public void Constructor_Default_CreatesEmptySeries()
{ {
@@ -284,9 +284,8 @@ namespace QuanTAlib.Tests
series.Add(100, 1.0); series.Add(100, 1.0);
series.Add(200, 2.0); series.Add(200, 2.0);
IEnumerable enumerable = series;
var list = new List<object>(); var list = new List<object>();
foreach (var item in enumerable) foreach (var item in (IEnumerable)series)
{ {
list.Add(item); list.Add(item);
} }
@@ -327,13 +326,10 @@ namespace QuanTAlib.Tests
{ {
var series = new TSeries(); var series = new TSeries();
Assert.Empty(series);
series.Add(100, 1.0); series.Add(100, 1.0);
Assert.Single(series); Assert.Single(series);
series.Add(200, 2.0); series.Add(200, 2.0);
Assert.Equal(2, series.Count); 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); public void Add(long time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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) public void Add(IEnumerable<double> values)
{ {
+13 -14
View File
@@ -74,24 +74,23 @@ public class CsvFeed : IFeed
if (parts.Length != 6) if (parts.Length != 6)
throw new FormatException($"Invalid CSV format at line {originalLineNumber}. Expected 6 columns, found {parts.Length}"); 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) throw new FormatException($"Failed to parse timestamp at line {originalLineNumber}: {line}");
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);
} }
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; return series;
+3 -2
View File
@@ -128,14 +128,15 @@ public class GBM : IFeed
// Update current bar (intra-bar tick) // Update current bar (intra-bar tick)
double z = NextNormal(); double z = NextNormal();
double price = _lastPrice * Math.Exp(_drift + _vol * z); double price = _lastPrice * Math.Exp(_drift + _vol * z);
double volume = 1000 + _rnd.NextDouble() * 1000; double additionalVolume = 1000 + _rnd.NextDouble() * 1000;
var bar = _currentBar; var bar = _currentBar;
double newClose = price; double newClose = price;
double newHigh = Math.Max(bar.High, newClose); double newHigh = Math.Max(bar.High, newClose);
double newLow = Math.Min(bar.Low, 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; _lastPrice = newClose;
} }
+1 -1
View File
@@ -11,7 +11,7 @@
<body> <body>
<div id="app"></div> <div id="app"></div>
<script> <script>
window.$docsify = { globalThis.$docsify = {
name: 'QuanTAlib', name: 'QuanTAlib',
repo: 'https://github.com/mihakralj/QuanTAlib', repo: 'https://github.com/mihakralj/QuanTAlib',
loadSidebar: true, loadSidebar: true,
+4 -3
View File
@@ -36,11 +36,12 @@
<InternalsVisibleTo Include="QuanTAlib.Tests" /> <InternalsVisibleTo Include="QuanTAlib.Tests" />
</ItemGroup> </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> <ItemGroup>
<PackageReference Include="GitVersion.MsBuild" Version="6.5.1"> <PackageReference Include="GitVersion.MsBuild" Version="6.5.1">
<PrivateAssets>all</PrivateAssets> <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(0));
Assert.Throws<ArgumentException>(() => new Alma(10, sigma: 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); var alma = new Alma(10);
Assert.NotNull(alma); Assert.NotNull(alma);
@@ -52,7 +54,7 @@ public class AlmaTests
for (int i = 0; i < 100; i++) for (int i = 0; i < 100; i++)
{ {
var bar = gbm.Next(isNew: true); var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close); series.Add(new TValue(bar.Time, bar.Close));
} }
// Streaming // Streaming
@@ -66,7 +68,7 @@ public class AlmaTests
var batchResults = almaBatch.Update(series); var batchResults = almaBatch.Update(series);
Assert.Equal(streamingResults.Count, batchResults.Count); 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); 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)); throw new ArgumentException("Period must be greater than 0", nameof(period));
if (sigma <= 0) if (sigma <= 0)
throw new ArgumentException("Sigma must be greater than 0", nameof(sigma)); 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; _period = period;
_offset = offset; _offset = offset;
@@ -225,8 +227,8 @@ public sealed class Alma : ITValuePublisher
} }
// Horizontal sum // 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_01_00_11_10).AsDouble()); // skipcq: CS-R1131
vSum = Avx.Add(vSum, Avx2.Permute4x64(vSum.AsUInt64(), 0b_00_00_00_01).AsDouble()); vSum = Avx.Add(vSum, Avx2.Permute4x64(vSum.AsUInt64(), 0b_00_00_00_01).AsDouble()); // skipcq: CS-R1131
sum = vSum.GetElement(0); sum = vSum.GetElement(0);
} }
+32 -23
View File
@@ -23,19 +23,19 @@ public class ConvTests
// 2: 1*0.5 + 2*1.0 = 2.5 // 2: 1*0.5 + 2*1.0 = 2.5
// 3: 2*0.5 + 3*1.0 = 4.0 // 3: 2*0.5 + 3*1.0 = 4.0
// 4: 3*0.5 + 4*1.0 = 5.5 // 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 conv = new Conv(kernel);
var result1 = conv.Update(new TValue(DateTime.UtcNow, 1)); var result1 = conv.Update(new TValue(DateTime.UtcNow, 1));
Assert.Equal(1.0, result1.Value); Assert.Equal(1.0, result1.Value);
var result2 = conv.Update(new TValue(DateTime.UtcNow, 2)); var result2 = conv.Update(new TValue(DateTime.UtcNow, 2));
Assert.Equal(2.5, result2.Value); Assert.Equal(2.5, result2.Value);
var result3 = conv.Update(new TValue(DateTime.UtcNow, 3)); var result3 = conv.Update(new TValue(DateTime.UtcNow, 3));
Assert.Equal(4.0, result3.Value); Assert.Equal(4.0, result3.Value);
var result4 = conv.Update(new TValue(DateTime.UtcNow, 4)); var result4 = conv.Update(new TValue(DateTime.UtcNow, 4));
Assert.Equal(5.5, result4.Value); Assert.Equal(5.5, result4.Value);
} }
@@ -43,22 +43,22 @@ public class ConvTests
[Fact] [Fact]
public void BarCorrection_UpdatesCorrectly() public void BarCorrection_UpdatesCorrectly()
{ {
var kernel = new double[] { 0.5, 1.0 }; double[] kernel = [0.5, 1.0];
var conv = new Conv(kernel); var conv = new Conv(kernel);
// 1 // 1
conv.Update(new TValue(DateTime.UtcNow, 1)); conv.Update(new TValue(DateTime.UtcNow, 1));
// 2 (isNew=true) -> 2.5 // 2 (isNew=true) -> 2.5
var res1 = conv.Update(new TValue(DateTime.UtcNow, 2), isNew: true); var res1 = conv.Update(new TValue(DateTime.UtcNow, 2), isNew: true);
Assert.Equal(2.5, res1.Value); Assert.Equal(2.5, res1.Value);
// Update 2 to 3 (isNew=false) // Update 2 to 3 (isNew=false)
// Buffer was [1, 2]. Now [1, 3]. // Buffer was [1, 2]. Now [1, 3].
// 1*0.5 + 3*1.0 = 3.5 // 1*0.5 + 3*1.0 = 3.5
var res2 = conv.Update(new TValue(DateTime.UtcNow, 3), isNew: false); var res2 = conv.Update(new TValue(DateTime.UtcNow, 3), isNew: false);
Assert.Equal(3.5, res2.Value); Assert.Equal(3.5, res2.Value);
// New bar 4 (isNew=true) // New bar 4 (isNew=true)
// Buffer was [1, 3]. New bar 4. Buffer becomes [3, 4]. // Buffer was [1, 3]. New bar 4. Buffer becomes [3, 4].
// 3*0.5 + 4*1.0 = 1.5 + 4 = 5.5 // 3*0.5 + 4*1.0 = 1.5 + 4 = 5.5
@@ -69,16 +69,16 @@ public class ConvTests
[Fact] [Fact]
public void NanHandling_UsesLastValid() 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); var conv = new Conv(kernel);
// 1 -> 1 // 1 -> 1
conv.Update(new TValue(DateTime.UtcNow, 1)); conv.Update(new TValue(DateTime.UtcNow, 1));
// NaN -> treated as 1. Buffer: [1, 1]. Result: 2. // NaN -> treated as 1. Buffer: [1, 1]. Result: 2.
var res = conv.Update(new TValue(DateTime.UtcNow, double.NaN)); var res = conv.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.Equal(2.0, res.Value); Assert.Equal(2.0, res.Value);
// 2 -> Buffer: [1, 2]. Result: 3. // 2 -> Buffer: [1, 2]. Result: 3.
res = conv.Update(new TValue(DateTime.UtcNow, 2)); res = conv.Update(new TValue(DateTime.UtcNow, 2));
Assert.Equal(3.0, res.Value); Assert.Equal(3.0, res.Value);
@@ -87,37 +87,46 @@ public class ConvTests
[Fact] [Fact]
public void StaticCalculate_MatchesObjectApi() public void StaticCalculate_MatchesObjectApi()
{ {
var kernel = new double[] { 0.5, 1.0 }; double[] kernel = [0.5, 1.0];
var source = new TSeries(); var source = new TSeries();
source.Add(new TValue(DateTime.UtcNow, 1)); source.Add(new TValue(DateTime.UtcNow, 1));
source.Add(new TValue(DateTime.UtcNow, 2)); source.Add(new TValue(DateTime.UtcNow, 2));
source.Add(new TValue(DateTime.UtcNow, 3)); source.Add(new TValue(DateTime.UtcNow, 3));
source.Add(new TValue(DateTime.UtcNow, 4)); source.Add(new TValue(DateTime.UtcNow, 4));
var result = Conv.Calculate(source, kernel); var result = Conv.Calculate(source, kernel);
Assert.Equal(1.0, result.Values[0]); Assert.Equal(1.0, result.Values[0]);
Assert.Equal(2.5, result.Values[1]); Assert.Equal(2.5, result.Values[1]);
Assert.Equal(4.0, result.Values[2]); Assert.Equal(4.0, result.Values[2]);
Assert.Equal(5.5, result.Values[3]); Assert.Equal(5.5, result.Values[3]);
} }
[Fact] [Fact]
public void Reset_ClearsState() public void Reset_ClearsState()
{ {
var kernel = new double[] { 1.0, 1.0 }; double[] kernel = [1.0, 1.0];
var conv = new Conv(kernel); var conv = new Conv(kernel);
conv.Update(new TValue(DateTime.UtcNow, 1)); conv.Update(new TValue(DateTime.UtcNow, 1));
conv.Update(new TValue(DateTime.UtcNow, 2)); conv.Update(new TValue(DateTime.UtcNow, 2));
Assert.True(conv.IsHot); Assert.True(conv.IsHot);
conv.Reset(); conv.Reset();
Assert.False(conv.IsHot); Assert.False(conv.IsHot);
Assert.Equal(0, conv.Last.Value); Assert.Equal(0, conv.Last.Value);
// Should behave as new // Should behave as new
var res = conv.Update(new TValue(DateTime.UtcNow, 1)); var res = conv.Update(new TValue(DateTime.UtcNow, 1));
Assert.Equal(1.0, res.Value); 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); Array.Copy(kernel, _kernel, _period);
_buffer = new RingBuffer(_period); _buffer = new RingBuffer(_period);
Name = $"Conv({_period})"; Name = $"Conv({_period})";
_lastValidValue = double.NaN;
_p_lastValidValue = double.NaN;
} }
public Conv(ITValuePublisher source, double[] kernel) : this(kernel) public Conv(ITValuePublisher source, double[] kernel) : this(kernel)
@@ -154,7 +156,7 @@ public sealed class Conv : ITValuePublisher
} }
else else
{ {
_lastValidValue = 0; _lastValidValue = double.NaN;
} }
_buffer.Clear(); _buffer.Clear();
@@ -516,7 +518,7 @@ public sealed class Conv : ITValuePublisher
// Use stackalloc for small kernels to avoid heap allocation // Use stackalloc for small kernels to avoid heap allocation
Span<double> window = period <= 256 ? stackalloc double[period] : new double[period]; 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 windowIdx = 0; // Points to where the NEXT value goes (circular)
int count = 0; int count = 0;
@@ -563,8 +565,8 @@ public sealed class Conv : ITValuePublisher
public void Reset() public void Reset()
{ {
_buffer.Clear(); _buffer.Clear();
_lastValidValue = 0; _lastValidValue = double.NaN;
_p_lastValidValue = 0; _p_lastValidValue = double.NaN;
_head = 0; _head = 0;
Last = default; Last = default;
} }
+1
View File
@@ -164,6 +164,7 @@ public sealed class Dema : ITValuePublisher
_p_state1 = s1; _p_state1 = s1;
_p_state2 = s2; _p_state2 = s2;
_lastValidValue = lastValid; _lastValidValue = lastValid;
_p_lastValidValue = lastValid;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]); Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v); return new TSeries(t, v);
+1
View File
@@ -176,6 +176,7 @@ public sealed class Ema : ITValuePublisher
sourceTimes.CopyTo(tSpan); sourceTimes.CopyTo(tSpan);
_p_state = _state; _p_state = _state;
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]); Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v); 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 lookback = _period + (int)Math.Sqrt(_period) + 10; // Sufficient lookback
int startIndex = Math.Max(0, len - lookback); int startIndex = Math.Max(0, len - lookback);
_sampleCount = startIndex;
for (int i = startIndex; i < len; i++) 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})"; Name = $"Kama({period}, {fastPeriod}, {slowPeriod})";
_kama = double.NaN; _kama = double.NaN;
_lastValidValue = double.NaN;
} }
public Kama(ITValuePublisher source, int period = 10, int fastPeriod = 2, int slowPeriod = 30) 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) public TValue Update(TValue input, bool isNew = true)
{ {
double val = GetValidValue(input.Value); double val = GetValidValue(input.Value);
if (double.IsNaN(val))
{
Last = new TValue(input.Time, double.NaN);
Pub?.Invoke(Last);
return Last;
}
if (isNew) if (isNew)
{ {
@@ -190,11 +197,11 @@ public sealed class Kama : ITValuePublisher
v.Add(outputSpan[i]); v.Add(outputSpan[i]);
} }
// Restore state by replaying last few bars // Restore state by replaying the entire series
// This is expensive but necessary to sync the object state // This is expensive but necessary to sync the object state correctly
// because KAMA is recursive (IIR) and depends on the full history.
Reset(); Reset();
int startIndex = Math.Max(0, len - _period - 1); for (int i = 0; i < len; i++)
for (int i = startIndex; i < len; i++)
{ {
Update(source[i]); Update(source[i]);
} }
@@ -220,7 +227,7 @@ public sealed class Kama : ITValuePublisher
double volatilitySum = 0; double volatilitySum = 0;
double kama = 0; double kama = 0;
bool kamaInitialized = false; bool kamaInitialized = false;
double lastValid = 0; double lastValid = double.NaN;
for (int i = 0; i < source.Length; i++) for (int i = 0; i < source.Length; i++)
{ {
@@ -230,6 +237,12 @@ public sealed class Kama : ITValuePublisher
else else
val = lastValid; val = lastValid;
if (double.IsNaN(val))
{
output[i] = double.NaN;
continue;
}
// Add to buffer // Add to buffer
double removed = buffer[bufferIdx]; double removed = buffer[bufferIdx];
buffer[bufferIdx] = val; buffer[bufferIdx] = val;
@@ -298,7 +311,7 @@ public sealed class Kama : ITValuePublisher
_volatilitySum = 0; _volatilitySum = 0;
_p_volatilitySum = 0; _p_volatilitySum = 0;
_lastDiffOut = 0; _lastDiffOut = 0;
_lastValidValue = 0; _lastValidValue = double.NaN;
Last = default; Last = default;
} }
} }
+3 -1
View File
@@ -271,15 +271,17 @@ public sealed class Lsma : ITValuePublisher
_lastValidValue = 0; _lastValidValue = 0;
} }
double lastProcessedValue = _lastValidValue;
for (int i = startIndex; i < len; i++) for (int i = startIndex; i < len; i++)
{ {
double val = GetValidValue(source.Values[i]); double val = GetValidValue(source.Values[i]);
UpdateState(val); UpdateState(val);
lastProcessedValue = val;
} }
_p_sum_y = _sum_y; _p_sum_y = _sum_y;
_p_sum_xy = _sum_xy; _p_sum_xy = _sum_xy;
_p_last_val = source.Values[len - 1]; _p_last_val = lastProcessedValue;
_p_lastValidValue = _lastValidValue; _p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]); 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); 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] [Fact]
@@ -62,4 +63,40 @@ public class MamaTests
Assert.True(eventFired); 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 else
{ {
// Initialization phase // Initialization phase
_sumPr += input.Value; _sumPr += price;
double avg = _index > 0 ? _sumPr / _index : input.Value; double avg = _index > 0 ? _sumPr / _index : price;
_mama = avg; _mama = avg;
_fama = avg; _fama = avg;
@@ -230,47 +230,14 @@ public sealed class Mama : ITValuePublisher
var v = new List<double>(len); var v = new List<double>(len);
var t = new List<long>(len); var t = new List<long>(len);
var temp = new Mama(_fastLimit, _slowLimit);
for (int i = 0; i < len; i++) for (int i = 0; i < len; i++)
{ {
var item = source[i]; var item = source[i];
var result = temp.Update(item); var result = Update(item);
v.Add(result.Value); v.Add(result.Value);
t.Add(item.Time); 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); return new TSeries(t, v);
} }
+4 -3
View File
@@ -157,6 +157,7 @@ public sealed class Sma : ITValuePublisher
if (startIndex > 0) if (startIndex > 0)
{ {
_lastValidValue = 0;
for (int i = startIndex - 1; i >= 0; i--) for (int i = startIndex - 1; i >= 0; i--)
{ {
if (double.IsFinite(source.Values[i])) if (double.IsFinite(source.Values[i]))
@@ -182,7 +183,7 @@ public sealed class Sma : ITValuePublisher
} }
_p_sum = _sum; _p_sum = _sum;
_p_lastInput = source.Values[len - 1]; _p_lastInput = GetValidValue(source.Values[len - 1]);
_p_lastValidValue = _lastValidValue; _p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]); 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 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); vShift1 = Avx.Blend(vZero, vShift1, 0b_1110);
var vP1 = Avx.Add(vDelta, vShift1); 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); vShift2 = Avx.Blend(vZero, vShift2, 0b_1100);
var vP2 = Avx.Add(vP1, vShift2); var vP2 = Avx.Add(vP1, vShift2);
+27
View File
@@ -1,3 +1,5 @@
using Xunit;
using System;
namespace QuanTAlib.Tests; namespace QuanTAlib.Tests;
@@ -101,4 +103,29 @@ public class T3Tests
// Check last values match // Check last values match
Assert.Equal(resSeries.Last.Value, resSpan[count-1], 1e-9); 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 _state = State.New();
private State _p_state = State.New(); private State _p_state = State.New();
private double _lastValidValue; private double _lastValidValue;
private double _p_lastValidValue;
/// <summary> /// <summary>
/// Display name for the indicator. /// Display name for the indicator.
@@ -157,10 +158,12 @@ public sealed class T3 : ITValuePublisher
if (isNew) if (isNew)
{ {
_p_state = _state; _p_state = _state;
_p_lastValidValue = _lastValidValue;
} }
else else
{ {
_state = _p_state; _state = _p_state;
_lastValidValue = _p_lastValidValue;
} }
double val = GetValidValue(input.Value); double val = GetValidValue(input.Value);
@@ -196,6 +199,7 @@ public sealed class T3 : ITValuePublisher
sourceTimes.CopyTo(tSpan); sourceTimes.CopyTo(tSpan);
_p_state = _state; _p_state = _state;
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]); Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v); return new TSeries(t, v);
@@ -283,6 +287,7 @@ public sealed class T3 : ITValuePublisher
_state = State.New(); _state = State.New();
_p_state = _state; _p_state = _state;
_lastValidValue = 0; _lastValidValue = 0;
_p_lastValidValue = 0;
Last = default; Last = default;
} }
} }
+6 -1
View File
@@ -61,6 +61,7 @@ public sealed class Tema : ITValuePublisher
private EmaState _p_state3 = EmaState.New(); private EmaState _p_state3 = EmaState.New();
private double _lastValidValue; private double _lastValidValue;
private double _p_lastValidValue;
public string Name { get; } public string Name { get; }
public TValue Last { get; private set; } public TValue Last { get; private set; }
@@ -83,7 +84,7 @@ public sealed class Tema : ITValuePublisher
public Tema(double alpha) 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; _alpha = alpha;
_decay = 1.0 - alpha; _decay = 1.0 - alpha;
@@ -98,12 +99,14 @@ public sealed class Tema : ITValuePublisher
_p_state1 = _state1; _p_state1 = _state1;
_p_state2 = _state2; _p_state2 = _state2;
_p_state3 = _state3; _p_state3 = _state3;
_p_lastValidValue = _lastValidValue;
} }
else else
{ {
_state1 = _p_state1; _state1 = _p_state1;
_state2 = _p_state2; _state2 = _p_state2;
_state3 = _p_state3; _state3 = _p_state3;
_lastValidValue = _p_lastValidValue;
} }
// EMA1 // EMA1
@@ -174,6 +177,7 @@ public sealed class Tema : ITValuePublisher
_p_state2 = s2; _p_state2 = s2;
_p_state3 = s3; _p_state3 = s3;
_lastValidValue = lastValid; _lastValidValue = lastValid;
_p_lastValidValue = lastValid;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]); Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v); return new TSeries(t, v);
@@ -343,6 +347,7 @@ public sealed class Tema : ITValuePublisher
_p_state2 = EmaState.New(); _p_state2 = EmaState.New();
_p_state3 = EmaState.New(); _p_state3 = EmaState.New();
_lastValidValue = 0; _lastValidValue = 0;
_p_lastValidValue = 0;
Last = default; Last = default;
} }
} }
+1 -1
View File
@@ -188,7 +188,7 @@ public sealed class Wma : ITValuePublisher
_p_sum = _sum; _p_sum = _sum;
_p_wsum = _wsum; _p_wsum = _wsum;
_p_lastInput = _lastValidValue; _p_lastInput = GetValidValue(source.Values[len - 1]);
_p_lastValidValue = _lastValidValue; _p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]); Last = new TValue(tSpan[len - 1], vSpan[len - 1]);