mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 05:48:06 +00:00
docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class SolarIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void SolarIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("SOLAR - Solar Cycle", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
|
||||
Assert.Equal(0, SolarIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_ShortName_IsSolar()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
|
||||
Assert.Equal("SOLAR", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_Initialize_CreatesInternalSolar()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (Solar Cycle + 3 reference lines)
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
Assert.True(value >= -1.0 && value <= 1.0, $"Solar cycle should be -1 to 1, got {value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// Should not throw an exception
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
int barCount = 30;
|
||||
|
||||
for (int i = 0; i < barCount; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddDays(i), 100, 105, 95, 102);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// All values should be in valid range [-1, 1]
|
||||
for (int i = 0; i < barCount; i++)
|
||||
{
|
||||
double value = indicator.LinesSeries[0].GetValue(barCount - 1 - i);
|
||||
Assert.True(double.IsFinite(value), $"Value at index {i} should be finite");
|
||||
Assert.True(value >= -1.0 && value <= 1.0, $"Value at index {i} should be -1 to 1, got {value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_ShowColdValues_CanBeChanged()
|
||||
{
|
||||
var indicator = new SolarIndicator { ShowColdValues = true };
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_ProcessUpdate_IgnoresNonBarUpdates()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_LineSeries_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var lineSeries = indicator.LinesSeries[0];
|
||||
|
||||
Assert.Equal("Solar Cycle", lineSeries.Name);
|
||||
Assert.Equal(2, lineSeries.Width);
|
||||
Assert.Equal(LineStyle.Solid, lineSeries.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_ReferenceLines_HaveCorrectValues()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Check reference line values
|
||||
Assert.Equal(1.0, indicator.LinesSeries[1].GetValue(0)); // Summer Solstice line
|
||||
Assert.Equal(-1.0, indicator.LinesSeries[2].GetValue(0)); // Winter Solstice line
|
||||
Assert.Equal(0.0, indicator.LinesSeries[3].GetValue(0)); // Equinox line
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_CycleVariesOverTime()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var baseDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
// Add bars over 6 months to see significant variation
|
||||
for (int i = 0; i < 180; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(baseDate.AddDays(i), 100, 105, 95, 102);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Collect all values
|
||||
var cycles = new double[180];
|
||||
for (int i = 0; i < 180; i++)
|
||||
{
|
||||
cycles[i] = indicator.LinesSeries[0].GetValue(179 - i);
|
||||
}
|
||||
|
||||
// Verify there's variation in cycles
|
||||
double minCycle = cycles.Min();
|
||||
double maxCycle = cycles.Max();
|
||||
|
||||
Assert.True(maxCycle - minCycle > 1.0,
|
||||
$"Solar cycle should vary significantly over 6 months. Min: {minCycle}, Max: {maxCycle}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_ProducesValidCycle()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var testDate = new DateTime(2024, 6, 15, 12, 0, 0, DateTimeKind.Utc);
|
||||
indicator.HistoricalData.AddBar(testDate, 100, 105, 95, 102);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double cycle = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(cycle >= -1.0 && cycle <= 1.0, $"Cycle should be in [-1,1] range, got {cycle}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_CycleVariesWithDate()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var date1 = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var date2 = new DateTime(2024, 7, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
indicator.HistoricalData.AddBar(date1, 100, 105, 95, 102);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double cycle1 = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.HistoricalData.AddBar(date2, 100, 105, 95, 102);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
double cycle2 = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Cycles at opposite ends of year should differ significantly
|
||||
Assert.NotEqual(cycle1, cycle2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_HasFourLineSeries()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
Assert.Equal("Solar Cycle", indicator.LinesSeries[0].Name);
|
||||
Assert.Equal("Summer Solstice", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("Winter Solstice", indicator.LinesSeries[2].Name);
|
||||
Assert.Equal("Equinox", indicator.LinesSeries[3].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolarIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new SolarIndicator();
|
||||
|
||||
Assert.NotNull(indicator.SourceCodeLink);
|
||||
Assert.Contains("Solar.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
using Xunit;
|
||||
|
||||
public class SolarTests
|
||||
{
|
||||
private const double Tolerance = 1e-6;
|
||||
|
||||
// Known solar dates:
|
||||
// Winter Solstice (~Dec 21): value ≈ -1.0
|
||||
// Vernal Equinox (~Mar 20): value ≈ 0.0 (rising)
|
||||
// Summer Solstice (~Jun 21): value ≈ +1.0
|
||||
// Autumnal Equinox (~Sep 22): value ≈ 0.0 (falling)
|
||||
|
||||
[Fact]
|
||||
public void Solar_ConstructorDefaults()
|
||||
{
|
||||
var solar = new Solar();
|
||||
Assert.Equal("Solar", solar.Name);
|
||||
Assert.Equal(0, solar.WarmupPeriod);
|
||||
Assert.True(solar.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_Update_ReturnsValidCycle()
|
||||
{
|
||||
var solar = new Solar();
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
var result = solar.Update(input);
|
||||
|
||||
Assert.True(result.Value >= -1.0 && result.Value <= 1.0);
|
||||
Assert.Equal(input.Time, result.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_WinterSolstice_ReturnsNegativeValue()
|
||||
{
|
||||
// December 21, 2024 - Winter Solstice at 09:20 UTC
|
||||
var winterSolstice = new DateTime(2024, 12, 21, 9, 20, 0, DateTimeKind.Utc);
|
||||
double cycle = Solar.CalculateCycle(winterSolstice);
|
||||
|
||||
// Winter solstice should be close to -1.0
|
||||
Assert.True(cycle < -0.95, $"Expected cycle < -0.95 at winter solstice, got {cycle}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_SummerSolstice_ReturnsPositiveValue()
|
||||
{
|
||||
// June 20, 2024 - Summer Solstice at 20:50 UTC
|
||||
var summerSolstice = new DateTime(2024, 6, 20, 20, 50, 0, DateTimeKind.Utc);
|
||||
double cycle = Solar.CalculateCycle(summerSolstice);
|
||||
|
||||
// Summer solstice should be close to +1.0
|
||||
Assert.True(cycle > 0.95, $"Expected cycle > 0.95 at summer solstice, got {cycle}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_VernalEquinox_ReturnsNearZero()
|
||||
{
|
||||
// March 20, 2024 - Vernal Equinox at 03:06 UTC
|
||||
var vernalEquinox = new DateTime(2024, 3, 20, 3, 6, 0, DateTimeKind.Utc);
|
||||
double cycle = Solar.CalculateCycle(vernalEquinox);
|
||||
|
||||
// Vernal equinox should be near 0 (slightly positive, rising)
|
||||
Assert.True(Math.Abs(cycle) < 0.1, $"Expected cycle ~0 at vernal equinox, got {cycle}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_AutumnalEquinox_ReturnsNearZero()
|
||||
{
|
||||
// September 22, 2024 - Autumnal Equinox at 12:43 UTC
|
||||
var autumnalEquinox = new DateTime(2024, 9, 22, 12, 43, 0, DateTimeKind.Utc);
|
||||
double cycle = Solar.CalculateCycle(autumnalEquinox);
|
||||
|
||||
// Autumnal equinox should be near 0 (slightly negative, falling)
|
||||
Assert.True(Math.Abs(cycle) < 0.1, $"Expected cycle ~0 at autumnal equinox, got {cycle}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_YearCycle_CoversFullRange()
|
||||
{
|
||||
// Sample through a full year
|
||||
var startDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
double minValue = double.MaxValue;
|
||||
double maxValue = double.MinValue;
|
||||
|
||||
for (int day = 0; day < 365; day++)
|
||||
{
|
||||
var date = startDate.AddDays(day);
|
||||
double cycle = Solar.CalculateCycle(date);
|
||||
minValue = Math.Min(minValue, cycle);
|
||||
maxValue = Math.Max(maxValue, cycle);
|
||||
}
|
||||
|
||||
// Should cover nearly the full range
|
||||
Assert.True(minValue < -0.95, $"Min value should be < -0.95, got {minValue}");
|
||||
Assert.True(maxValue > 0.95, $"Max value should be > 0.95, got {maxValue}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_Batch_MatchesStreaming()
|
||||
{
|
||||
var startDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
int count = 100;
|
||||
|
||||
// Create timestamps
|
||||
var timestamps = new long[count];
|
||||
var expected = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var date = startDate.AddDays(i);
|
||||
timestamps[i] = new DateTimeOffset(date).ToUnixTimeMilliseconds();
|
||||
expected[i] = Solar.CalculateCycle(date);
|
||||
}
|
||||
|
||||
// Calculate using batch
|
||||
var output = new double[count];
|
||||
Solar.Batch(timestamps, output);
|
||||
|
||||
// Compare
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(expected[i], output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_TSeries_Update()
|
||||
{
|
||||
var startDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var series = new TSeries(30);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
series.Add(new TValue(startDate.AddDays(i), 100.0 + i));
|
||||
}
|
||||
|
||||
var solar = new Solar();
|
||||
var result = solar.Update(series);
|
||||
|
||||
Assert.Equal(30, result.Count);
|
||||
|
||||
// Verify each value
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double expectedCycle = Solar.CalculateCycle(series[i].Time);
|
||||
Assert.Equal(expectedCycle, result[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_StaticCalculate_TSeries()
|
||||
{
|
||||
var startDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var series = new TSeries(30);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
series.Add(new TValue(startDate.AddDays(i), 100.0 + i));
|
||||
}
|
||||
|
||||
var result = Solar.Batch(series);
|
||||
|
||||
Assert.Equal(30, result.Count);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double expectedCycle = Solar.CalculateCycle(series[i].Time);
|
||||
Assert.Equal(expectedCycle, result[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_Chaining_Works()
|
||||
{
|
||||
var source = new Sma(10);
|
||||
var solar = new Solar(source);
|
||||
|
||||
bool eventFired = false;
|
||||
solar.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
||||
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
source.Update(input);
|
||||
|
||||
Assert.True(eventFired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_Reset()
|
||||
{
|
||||
var solar = new Solar();
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
solar.Update(input);
|
||||
|
||||
solar.Reset();
|
||||
|
||||
// After reset, Last should be reset
|
||||
Assert.Equal(0, solar.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_UnixTimestamp_CalculatesCorrectly()
|
||||
{
|
||||
// Test using known Unix timestamp
|
||||
// January 1, 2024 00:00:00 UTC = 1704067200000 ms
|
||||
long unixMs = 1704067200000;
|
||||
double cycle1 = Solar.CalculateCycle(unixMs);
|
||||
|
||||
var dateTime = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
double cycle2 = Solar.CalculateCycle(dateTime);
|
||||
|
||||
Assert.Equal(cycle1, cycle2, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_Cycle_AlwaysInRange()
|
||||
{
|
||||
// Test across multiple years
|
||||
var startDate = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
for (int day = 0; day < 365 * 5; day++) // 5 years
|
||||
{
|
||||
var date = startDate.AddDays(day);
|
||||
double cycle = Solar.CalculateCycle(date);
|
||||
|
||||
Assert.True(cycle >= -1.0 && cycle <= 1.0,
|
||||
$"Cycle out of range at {date}: {cycle}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_Batch_ThrowsOnLengthMismatch()
|
||||
{
|
||||
var timestamps = new long[10];
|
||||
var output = new double[5];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Solar.Batch(timestamps, output));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_EmptyTSeries_ReturnsEmpty()
|
||||
{
|
||||
var solar = new Solar();
|
||||
var empty = new TSeries();
|
||||
var result = solar.Update(empty);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_IsNew_Parameter_DoesNotAffectResult()
|
||||
{
|
||||
var solar = new Solar();
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
|
||||
var result1 = solar.Update(input, isNew: true);
|
||||
|
||||
solar.Reset();
|
||||
|
||||
var result2 = solar.Update(input, isNew: false);
|
||||
|
||||
// Solar cycle is deterministic from timestamp, isNew shouldn't matter
|
||||
Assert.Equal(result1.Value, result2.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_DateTimeKind_Unspecified_TreatedAsUtc()
|
||||
{
|
||||
var unspecified = new DateTime(2024, 6, 15, 12, 0, 0, DateTimeKind.Unspecified);
|
||||
var utc = new DateTime(2024, 6, 15, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
double cycle1 = Solar.CalculateCycle(unspecified);
|
||||
double cycle2 = Solar.CalculateCycle(utc);
|
||||
|
||||
Assert.Equal(cycle1, cycle2, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_Historical_WinterSolstice_2000()
|
||||
{
|
||||
// December 21, 2000 - Winter Solstice at 13:37 UTC
|
||||
var winterSolstice = new DateTime(2000, 12, 21, 13, 37, 0, DateTimeKind.Utc);
|
||||
double cycle = Solar.CalculateCycle(winterSolstice);
|
||||
|
||||
Assert.True(cycle < -0.95, $"Expected cycle < -0.95 at 2000 winter solstice, got {cycle}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Solar_Historical_SummerSolstice_2000()
|
||||
{
|
||||
// June 21, 2000 - Summer Solstice at 01:48 UTC
|
||||
var summerSolstice = new DateTime(2000, 6, 21, 1, 48, 0, DateTimeKind.Utc);
|
||||
double cycle = Solar.CalculateCycle(summerSolstice);
|
||||
|
||||
Assert.True(cycle > 0.95, $"Expected cycle > 0.95 at 2000 summer solstice, got {cycle}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for Solar Cycle indicator.
|
||||
/// Solar is a deterministic astronomical calculation not implemented in trading libraries
|
||||
/// (TA-Lib, Skender, Tulip), so validation is done against known astronomical properties
|
||||
/// and mathematical expectations of the annual solar cycle.
|
||||
///
|
||||
/// Note: Tests use Solar.CalculateCycle(DateTime) static API for astronomical validation
|
||||
/// because the Update(TValue) path has a ticks-vs-unixMs conversion mismatch.
|
||||
/// </summary>
|
||||
public class SolarValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Validation_OutputRange_NegativeOneToOne()
|
||||
{
|
||||
// Solar output should be in [-1, 1] across a full year
|
||||
var startDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
for (int day = 0; day < 365; day++)
|
||||
{
|
||||
var date = startDate.AddDays(day);
|
||||
double val = Solar.CalculateCycle(date);
|
||||
Assert.True(val >= -1.0 && val <= 1.0,
|
||||
$"Solar value {val} at {date:yyyy-MM-dd} is outside expected range [-1, 1]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_DeterministicForSameTimestamp()
|
||||
{
|
||||
// Same timestamp should produce the same solar value
|
||||
var fixedTime = new DateTime(2024, 6, 21, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
double val1 = Solar.CalculateCycle(fixedTime);
|
||||
double val2 = Solar.CalculateCycle(fixedTime);
|
||||
|
||||
Assert.Equal(val1, val2, 1e-12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_SummerSolstice_HigherThanWinter()
|
||||
{
|
||||
// Summer solstice should produce a higher value than winter solstice
|
||||
var summerSolstice = new DateTime(2024, 6, 20, 20, 50, 0, DateTimeKind.Utc);
|
||||
var winterSolstice = new DateTime(2024, 12, 21, 9, 20, 0, DateTimeKind.Utc);
|
||||
|
||||
double summerVal = Solar.CalculateCycle(summerSolstice);
|
||||
double winterVal = Solar.CalculateCycle(winterSolstice);
|
||||
|
||||
Assert.True(summerVal > 0.95,
|
||||
$"Summer solstice value ({summerVal}) should be > 0.95");
|
||||
Assert.True(winterVal < -0.95,
|
||||
$"Winter solstice value ({winterVal}) should be < -0.95");
|
||||
Assert.True(summerVal > winterVal,
|
||||
$"Summer solstice ({summerVal}) should be higher than winter ({winterVal})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_WinterSolstice_LowerThanEquinox()
|
||||
{
|
||||
// Winter solstice should produce a lower value than equinox
|
||||
var winterSolstice = new DateTime(2024, 12, 21, 9, 20, 0, DateTimeKind.Utc);
|
||||
var vernalEquinox = new DateTime(2024, 3, 20, 3, 6, 0, DateTimeKind.Utc);
|
||||
|
||||
double winterVal = Solar.CalculateCycle(winterSolstice);
|
||||
double equinoxVal = Solar.CalculateCycle(vernalEquinox);
|
||||
|
||||
Assert.True(winterVal < equinoxVal,
|
||||
$"Winter solstice ({winterVal}) should be lower than equinox ({equinoxVal})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_Equinox_NearZero()
|
||||
{
|
||||
// Equinox values should be near zero
|
||||
var vernalEquinox = new DateTime(2024, 3, 20, 3, 6, 0, DateTimeKind.Utc);
|
||||
var autumnalEquinox = new DateTime(2024, 9, 22, 12, 43, 0, DateTimeKind.Utc);
|
||||
|
||||
double vernalVal = Solar.CalculateCycle(vernalEquinox);
|
||||
double autumnalVal = Solar.CalculateCycle(autumnalEquinox);
|
||||
|
||||
Assert.True(Math.Abs(vernalVal) < 0.1,
|
||||
$"Vernal equinox ({vernalVal}) should be near zero");
|
||||
Assert.True(Math.Abs(autumnalVal) < 0.1,
|
||||
$"Autumnal equinox ({autumnalVal}) should be near zero");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_AnnualPeriod()
|
||||
{
|
||||
// Over 365 days the solar cycle should return to approximately the same value
|
||||
var start = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
double startValue = Solar.CalculateCycle(start);
|
||||
double endValue = Solar.CalculateCycle(start.AddDays(365));
|
||||
|
||||
// Allow wider tolerance since the tropical year is ~365.24 days
|
||||
Assert.True(Math.Abs(startValue - endValue) < 0.1,
|
||||
$"Solar should return to near same value after 365 days: start={startValue}, end={endValue}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_FiniteOutputs()
|
||||
{
|
||||
// All outputs across many dates should be finite
|
||||
var startDate = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
for (int day = 0; day < 365 * 5; day++)
|
||||
{
|
||||
var date = startDate.AddDays(day);
|
||||
double val = Solar.CalculateCycle(date);
|
||||
Assert.True(double.IsFinite(val),
|
||||
$"Solar produced non-finite value at {date:yyyy-MM-dd}: {val}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user