mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-12 15:48:05 +00:00
Refactor documentation links in numerics, oscillators, reversals, and statistics modules to use relative paths; update Bias class to handle division by zero more robustly; remove obsolete CUMMEAN Pine script; enhance trend indicators documentation; add Visual Studio Code workspace configuration.
This commit is contained in:
@@ -1,360 +0,0 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validates the behavioral differences between .NET's Math.Atan2 and PineScript's custom atan2 implementation.
|
||||
///
|
||||
/// PineScript's atan2 uses a numerically stable algorithm:
|
||||
/// 1. If |x| > |y|: angle = atan(|y|/|x|)
|
||||
/// 2. If |y| >= |x|: angle = π/2 - atan(|x|/|y|)
|
||||
/// 3. Then applies quadrant correction based on sign of x and y
|
||||
///
|
||||
/// Math.Atan2 follows the standard IEEE convention: atan2(y, x) returns the angle
|
||||
/// in radians between the positive x-axis and the point (x, y).
|
||||
///
|
||||
/// Both return values in the range [-π, π], but they can differ in edge cases
|
||||
/// and have different numerical stability characteristics.
|
||||
/// </summary>
|
||||
public class Atan2ValidationTests
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public Atan2ValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PineScript-style atan2 implementation for comparison.
|
||||
/// This is a direct port of the algorithm from ht_phasor.pine.
|
||||
/// </summary>
|
||||
private static double PineScriptAtan2(double y, double x)
|
||||
{
|
||||
if (y == 0.0 && x == 0.0)
|
||||
{
|
||||
throw new ArgumentException("atan2: Both y and x cannot be zero", nameof(y));
|
||||
}
|
||||
|
||||
double ay = Math.Abs(y);
|
||||
double ax = Math.Abs(x);
|
||||
double angle;
|
||||
|
||||
if (ax > ay)
|
||||
{
|
||||
angle = Math.Atan(ay / ax);
|
||||
}
|
||||
else
|
||||
{
|
||||
angle = (Math.PI / 2.0) - Math.Atan(ax / ay);
|
||||
}
|
||||
|
||||
if (x < 0.0)
|
||||
{
|
||||
angle = Math.PI - angle;
|
||||
}
|
||||
|
||||
if (y < 0.0)
|
||||
{
|
||||
angle = -angle;
|
||||
}
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_StandardQuadrant1_BothMatch()
|
||||
{
|
||||
// First quadrant: x > 0, y > 0
|
||||
double y = 1.0, x = 1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Q1: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Q1: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Q1: Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
Assert.Equal(dotNet, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_StandardQuadrant2_BothMatch()
|
||||
{
|
||||
// Second quadrant: x < 0, y > 0
|
||||
double y = 1.0, x = -1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Q2: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Q2: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Q2: Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
Assert.Equal(dotNet, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_StandardQuadrant3_BothMatch()
|
||||
{
|
||||
// Third quadrant: x < 0, y < 0
|
||||
double y = -1.0, x = -1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Q3: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Q3: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Q3: Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
Assert.Equal(dotNet, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_StandardQuadrant4_BothMatch()
|
||||
{
|
||||
// Fourth quadrant: x > 0, y < 0
|
||||
double y = -1.0, x = 1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Q4: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Q4: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Q4: Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
Assert.Equal(dotNet, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_AxisAligned_PositiveY()
|
||||
{
|
||||
// On positive Y-axis: x = 0, y > 0 → π/2
|
||||
double y = 1.0, x = 0.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double expected = Math.PI / 2.0;
|
||||
|
||||
_output.WriteLine($"Positive Y-axis: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Positive Y-axis: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Expected: π/2 = {expected:F15}");
|
||||
|
||||
Assert.Equal(expected, dotNet, precision: 14);
|
||||
Assert.Equal(expected, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_AxisAligned_NegativeY()
|
||||
{
|
||||
// On negative Y-axis: x = 0, y < 0 → -π/2
|
||||
double y = -1.0, x = 0.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double expected = -Math.PI / 2.0;
|
||||
|
||||
_output.WriteLine($"Negative Y-axis: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Negative Y-axis: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Expected: -π/2 = {expected:F15}");
|
||||
|
||||
Assert.Equal(expected, dotNet, precision: 14);
|
||||
Assert.Equal(expected, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_AxisAligned_PositiveX()
|
||||
{
|
||||
// On positive X-axis: x > 0, y = 0 → 0
|
||||
double y = 0.0, x = 1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double expected = 0.0;
|
||||
|
||||
_output.WriteLine($"Positive X-axis: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Positive X-axis: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Expected: 0 = {expected:F15}");
|
||||
|
||||
Assert.Equal(expected, dotNet, precision: 14);
|
||||
Assert.Equal(expected, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_AxisAligned_NegativeX()
|
||||
{
|
||||
// On negative X-axis: x < 0, y = 0 → π
|
||||
double y = 0.0, x = -1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double expected = Math.PI;
|
||||
|
||||
_output.WriteLine($"Negative X-axis: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Negative X-axis: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Expected: π = {expected:F15}");
|
||||
|
||||
Assert.Equal(expected, dotNet, precision: 14);
|
||||
Assert.Equal(expected, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_Origin_DotNetReturnsZero_PineThrows()
|
||||
{
|
||||
// Origin: x = 0, y = 0
|
||||
// Math.Atan2 returns 0 (by convention)
|
||||
// PineScript throws an error
|
||||
double y = 0.0, x = 0.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
|
||||
_output.WriteLine($"Origin: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine("Origin: PineScript throws ArgumentException");
|
||||
|
||||
Assert.Equal(0.0, dotNet);
|
||||
Assert.Throws<ArgumentException>(() => PineScriptAtan2(y, x));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_VerySmallValues_NumericalStability()
|
||||
{
|
||||
// Test numerical stability with very small values
|
||||
double y = 1e-300, x = 1e-300;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double expected = Math.PI / 4.0; // 45 degrees
|
||||
|
||||
_output.WriteLine($"Small values: Math.Atan2({y:E}, {x:E}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Small values: PineAtan2({y:E}, {x:E}) = {pine:F15}");
|
||||
_output.WriteLine($"Expected: π/4 = {expected:F15}");
|
||||
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
// Both should handle small values well
|
||||
Assert.Equal(expected, dotNet, precision: 10);
|
||||
Assert.Equal(expected, pine, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_VeryLargeValues_NumericalStability()
|
||||
{
|
||||
// Test numerical stability with very large values
|
||||
double y = 1e300, x = 1e300;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double expected = Math.PI / 4.0; // 45 degrees
|
||||
|
||||
_output.WriteLine($"Large values: Math.Atan2({y:E}, {x:E}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Large values: PineAtan2({y:E}, {x:E}) = {pine:F15}");
|
||||
_output.WriteLine($"Expected: π/4 = {expected:F15}");
|
||||
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
Assert.Equal(expected, dotNet, precision: 10);
|
||||
Assert.Equal(expected, pine, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_AspectRatioExtreme_TallVector()
|
||||
{
|
||||
// PineScript algorithm switches behavior when |y| > |x|
|
||||
// Test with y >> x
|
||||
double y = 1000.0, x = 1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Tall vector: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Tall vector: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
// Should be very close to π/2
|
||||
Assert.True(Math.Abs(dotNet - pine) < 1e-12, $"Difference {Math.Abs(dotNet - pine):E} exceeds tolerance");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_AspectRatioExtreme_WideVector()
|
||||
{
|
||||
// Test with x >> y
|
||||
double y = 1.0, x = 1000.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Wide vector: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Wide vector: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
// Should be very close to 0
|
||||
Assert.True(Math.Abs(dotNet - pine) < 1e-12, $"Difference {Math.Abs(dotNet - pine):E} exceeds tolerance");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.5, 0.866025403784439)] // 30 degrees
|
||||
[InlineData(0.707106781186548, 0.707106781186548)] // 45 degrees
|
||||
[InlineData(0.866025403784439, 0.5)] // 60 degrees
|
||||
public void Atan2_CommonAngles_Match(double y, double x)
|
||||
{
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Math.Atan2({y}, {x}) = {dotNet:F15} rad = {dotNet * 180 / Math.PI:F10}°");
|
||||
_output.WriteLine($"PineAtan2({y}, {x}) = {pine:F15} rad = {pine * 180 / Math.PI:F10}°");
|
||||
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
Assert.Equal(dotNet, pine, precision: 13);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_FullCircle_360DegreesSweep()
|
||||
{
|
||||
// Sweep through 360 degrees and verify both implementations match
|
||||
const int steps = 360;
|
||||
double maxDiff = 0;
|
||||
int maxDiffStep = 0;
|
||||
|
||||
for (int i = 0; i < steps; i++)
|
||||
{
|
||||
double angle = i * 2.0 * Math.PI / steps;
|
||||
double y = Math.Sin(angle);
|
||||
double x = Math.Cos(angle);
|
||||
|
||||
// Skip origin (angle = 0 with x=1, y=0 is fine, but need to handle numerical zeros)
|
||||
if (Math.Abs(x) < 1e-15 && Math.Abs(y) < 1e-15)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double diff = Math.Abs(dotNet - pine);
|
||||
|
||||
if (diff > maxDiff)
|
||||
{
|
||||
maxDiff = diff;
|
||||
maxDiffStep = i;
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine($"Full circle sweep: {steps} steps");
|
||||
_output.WriteLine($"Maximum difference: {maxDiff:E} at step {maxDiffStep} ({maxDiffStep}°)");
|
||||
|
||||
// Expect very small differences
|
||||
Assert.True(maxDiff < 1e-14, $"Maximum difference {maxDiff:E} exceeds tolerance");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Summary_ImplementationDifferences()
|
||||
{
|
||||
_output.WriteLine("=== Math.Atan2 vs PineScript atan2 Summary ===");
|
||||
_output.WriteLine("");
|
||||
_output.WriteLine("1. Origin handling:");
|
||||
_output.WriteLine(" - Math.Atan2(0, 0) returns 0");
|
||||
_output.WriteLine(" - PineScript throws an error");
|
||||
_output.WriteLine("");
|
||||
_output.WriteLine("2. Algorithm:");
|
||||
_output.WriteLine(" - Math.Atan2: IEEE standard implementation");
|
||||
_output.WriteLine(" - PineScript: Uses |y|/|x| or |x|/|y| based on which is larger");
|
||||
_output.WriteLine(" This avoids division by very small numbers");
|
||||
_output.WriteLine("");
|
||||
_output.WriteLine("3. Numerical stability:");
|
||||
_output.WriteLine(" - Both handle extreme values well");
|
||||
_output.WriteLine(" - PineScript's approach may be slightly more stable for extreme aspect ratios");
|
||||
_output.WriteLine("");
|
||||
_output.WriteLine("4. Recommendation:");
|
||||
_output.WriteLine(" - Use Math.Atan2 for general purposes (standard, well-tested)");
|
||||
_output.WriteLine(" - PineScript algorithm adds ~zero benefit in .NET");
|
||||
_output.WriteLine(" - Only difference is origin handling (error vs 0)");
|
||||
|
||||
Assert.True(true); // This is a documentation test
|
||||
}
|
||||
}
|
||||
+15
-15
@@ -6,18 +6,18 @@ Basic mathematical transforms and utility functions for time series. These build
|
||||
|
||||
| Indicator | Full Name | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| [ACCEL](/lib/numerics/accel/Accel.md) | Acceleration | Momentum change; second derivative of price. |
|
||||
| [CHANGE](/lib/numerics/change/Change.md) | Percentage Change | Relative price movement over lookback period. |
|
||||
| [EXPTRANS](/lib/numerics/exptrans/Exptrans.md) | Exponential Transform | e^x transform for log-space conversion reversal. |
|
||||
| [HIGHEST](/lib/numerics/highest/Highest.md) | Rolling Maximum | Maximum value over lookback window. |
|
||||
| [JERK](/lib/numerics/jerk/Jerk.md) | Jerk | Rate of acceleration; third derivative of price. |
|
||||
| [LINEARTRANS](/lib/numerics/lineartrans/Lineartrans.md) | Linear Transform | y = ax + b scaling transformation. |
|
||||
| [LOGTRANS](/lib/numerics/logtrans/Logtrans.md) | Logarithmic Transform | Natural log for percentage-based analysis. |
|
||||
| [LOWEST](/lib/numerics/lowest/Lowest.md) | Rolling Minimum | Minimum value over lookback window. |
|
||||
| [MIDPOINT](/lib/numerics/midpoint/Midpoint.md) | Midrange | (Highest + Lowest) / 2 over lookback window. |
|
||||
| [NORMALIZE](/lib/numerics/normalize/Normalize.md) | Min-Max Normalization | Scale to [0,1] range using rolling min/max. |
|
||||
| [RELU](/lib/numerics/relu/Relu.md) | Rectified Linear Unit | max(0, x); neural network activation function. |
|
||||
| [SIGMOID](/lib/numerics/sigmoid/Sigmoid.md) | Logistic Function | 1/(1+e^-x); bounded [0,1] transform. |
|
||||
| [SLOPE](/lib/numerics/slope/Slope.md) | Rate of Change | First derivative; velocity of price movement. |
|
||||
| [SQRTTRANS](/lib/numerics/sqrttrans/Sqrttrans.md) | Square Root Transform | Variance-stabilizing transformation. |
|
||||
| [STANDARDIZE](/lib/numerics/standardize/Standardize.md) | Z-Score Normalization | (x - mean) / stddev; zero-mean unit-variance transform. |
|
||||
| [ACCEL](accel/Accel.md) | Acceleration | Momentum change; second derivative of price. |
|
||||
| [CHANGE](change/Change.md) | Percentage Change | Relative price movement over lookback period. |
|
||||
| [EXPTRANS](exptrans/Exptrans.md) | Exponential Transform | e^x transform for log-space conversion reversal. |
|
||||
| [HIGHEST](highest/Highest.md) | Rolling Maximum | Maximum value over lookback window. |
|
||||
| [JERK](jerk/Jerk.md) | Jerk | Rate of acceleration; third derivative of price. |
|
||||
| [LINEARTRANS](lineartrans/Lineartrans.md) | Linear Transform | y = ax + b scaling transformation. |
|
||||
| [LOGTRANS](logtrans/Logtrans.md) | Logarithmic Transform | Natural log for percentage-based analysis. |
|
||||
| [LOWEST](lowest/Lowest.md) | Rolling Minimum | Minimum value over lookback window. |
|
||||
| [MIDPOINT](midpoint/Midpoint.md) | Midrange | (Highest + Lowest) / 2 over lookback window. |
|
||||
| [NORMALIZE](normalize/Normalize.md) | Min-Max Normalization | Scale to [0,1] range using rolling min/max. |
|
||||
| [RELU](relu/Relu.md) | Rectified Linear Unit | max(0, x); neural network activation function. |
|
||||
| [SIGMOID](sigmoid/Sigmoid.md) | Logistic Function | 1/(1+e^-x); bounded [0,1] transform. |
|
||||
| [SLOPE](slope/Slope.md) | Rate of Change | First derivative; velocity of price movement. |
|
||||
| [SQRTTRANS](sqrttrans/Sqrttrans.md) | Square Root Transform | Variance-stabilizing transformation. |
|
||||
| [STANDARDIZE](standardize/Standardize.md) | Z-Score Normalization | (x - mean) / stddev; zero-mean unit-variance transform. |
|
||||
|
||||
Reference in New Issue
Block a user