mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-07 13:37:44 +00:00
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:
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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/" \;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -11,7 +11,7 @@
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
window.$docsify = {
|
||||
globalThis.$docsify = {
|
||||
name: 'QuanTAlib',
|
||||
repo: 'https://github.com/mihakralj/QuanTAlib',
|
||||
loadSidebar: true,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user