mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-06 13:07:44 +00:00
361 lines
12 KiB
C#
361 lines
12 KiB
C#
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
|
|
}
|
|
}
|