mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-26 06:18:05 +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,269 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class LunarIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void LunarIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("LUNAR - Lunar Phase", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
|
||||
Assert.Equal(0, LunarIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_ShortName_IsLunar()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
|
||||
Assert.Equal("LUNAR", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_Initialize_CreatesInternalLunar()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (Lunar Phase + 3 reference lines)
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
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 >= 0.0 && value <= 1.0, $"Lunar phase should be 0-1, got {value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
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 LunarIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// Should not throw an exception
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
int barCount = 30; // Cover roughly one lunar month
|
||||
|
||||
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 [0, 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 >= 0.0 && value <= 1.0, $"Value at index {i} should be 0-1, got {value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_ShowColdValues_CanBeChanged()
|
||||
{
|
||||
var indicator = new LunarIndicator { ShowColdValues = true };
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_ProcessUpdate_IgnoresNonBarUpdates()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process historical bar first
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Process other update reasons - should not throw
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_LineSeries_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var lineSeries = indicator.LinesSeries[0];
|
||||
|
||||
Assert.Equal("Lunar Phase", lineSeries.Name);
|
||||
Assert.Equal(2, lineSeries.Width);
|
||||
Assert.Equal(LineStyle.Solid, lineSeries.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_ReferenceLines_HaveCorrectValues()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
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(0.0, indicator.LinesSeries[1].GetValue(0)); // New Moon line
|
||||
Assert.Equal(1.0, indicator.LinesSeries[2].GetValue(0)); // Full Moon line
|
||||
Assert.Equal(0.5, indicator.LinesSeries[3].GetValue(0)); // Quarter line
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_PhaseVariesOverTime()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var baseDate = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
// Add bars over a lunar month
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(baseDate.AddDays(i), 100, 105, 95, 102);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Collect all values
|
||||
var phases = new double[30];
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
phases[i] = indicator.LinesSeries[0].GetValue(29 - i);
|
||||
}
|
||||
|
||||
// Verify there's variation in phases (not all same value)
|
||||
double minPhase = phases.Min();
|
||||
double maxPhase = phases.Max();
|
||||
|
||||
Assert.True(maxPhase - minPhase > 0.5,
|
||||
$"Lunar phase should vary significantly over a month. Min: {minPhase}, Max: {maxPhase}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_ProducesValidPhase()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// Use any date - the phase should be in valid range
|
||||
var testDate = new DateTime(2025, 1, 15, 12, 0, 0, DateTimeKind.Utc);
|
||||
indicator.HistoricalData.AddBar(testDate, 100, 105, 95, 102);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double phase = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(phase >= 0.0 && phase <= 1.0, $"Phase should be in [0,1] range, got {phase}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_PhaseVariesWithDate()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// Add bars at different dates and verify phases vary
|
||||
var date1 = new DateTime(2025, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var date2 = new DateTime(2025, 1, 15, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
indicator.HistoricalData.AddBar(date1, 100, 105, 95, 102);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double phase1 = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.HistoricalData.AddBar(date2, 100, 105, 95, 102);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
double phase2 = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Phases at different dates should differ (14 days apart = significant lunar change)
|
||||
Assert.NotEqual(phase1, phase2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_HasFourLineSeries()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
Assert.Equal("Lunar Phase", indicator.LinesSeries[0].Name);
|
||||
Assert.Equal("New Moon", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("Full Moon", indicator.LinesSeries[2].Name);
|
||||
Assert.Equal("Quarter", indicator.LinesSeries[3].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LunarIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new LunarIndicator();
|
||||
|
||||
Assert.NotNull(indicator.SourceCodeLink);
|
||||
Assert.Contains("Lunar.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
using Xunit;
|
||||
|
||||
public class LunarTests
|
||||
{
|
||||
private const double Tolerance = 1e-6;
|
||||
|
||||
// Known lunar phase dates (verified against astronomical data)
|
||||
// New Moon: ~0.0, Full Moon: ~1.0, Quarters: ~0.5
|
||||
|
||||
[Fact]
|
||||
public void Lunar_ConstructorDefaults()
|
||||
{
|
||||
var lunar = new Lunar();
|
||||
Assert.Equal("Lunar", lunar.Name);
|
||||
Assert.Equal(0, lunar.WarmupPeriod);
|
||||
Assert.True(lunar.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_Update_ReturnsValidPhase()
|
||||
{
|
||||
var lunar = new Lunar();
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
var result = lunar.Update(input);
|
||||
|
||||
Assert.True(result.Value >= 0.0 && result.Value <= 1.0);
|
||||
Assert.Equal(input.Time, result.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_KnownNewMoon_ReturnsLowPhase()
|
||||
{
|
||||
// January 29, 2025 - New Moon at 12:36 UTC
|
||||
var newMoon = new DateTime(2025, 1, 29, 12, 36, 0, DateTimeKind.Utc);
|
||||
double phase = Lunar.CalculatePhase(newMoon);
|
||||
|
||||
// New moon should be close to 0
|
||||
Assert.True(phase < 0.05, $"Expected phase < 0.05 at new moon, got {phase}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_KnownFullMoon_ReturnsHighPhase()
|
||||
{
|
||||
// February 12, 2025 - Full Moon at 13:53 UTC
|
||||
var fullMoon = new DateTime(2025, 2, 12, 13, 53, 0, DateTimeKind.Utc);
|
||||
double phase = Lunar.CalculatePhase(fullMoon);
|
||||
|
||||
// Full moon should be close to 1
|
||||
Assert.True(phase > 0.95, $"Expected phase > 0.95 at full moon, got {phase}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_FirstQuarter_ReturnsHalfPhase()
|
||||
{
|
||||
// February 5, 2025 - First Quarter at 08:02 UTC
|
||||
var firstQuarter = new DateTime(2025, 2, 5, 8, 2, 0, DateTimeKind.Utc);
|
||||
double phase = Lunar.CalculatePhase(firstQuarter);
|
||||
|
||||
// First quarter should be around 0.5
|
||||
Assert.True(phase > 0.4 && phase < 0.6, $"Expected phase ~0.5 at first quarter, got {phase}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_LastQuarter_ReturnsHalfPhase()
|
||||
{
|
||||
// February 20, 2025 - Last Quarter at 17:33 UTC
|
||||
var lastQuarter = new DateTime(2025, 2, 20, 17, 33, 0, DateTimeKind.Utc);
|
||||
double phase = Lunar.CalculatePhase(lastQuarter);
|
||||
|
||||
// Last quarter should be around 0.5
|
||||
Assert.True(phase > 0.4 && phase < 0.6, $"Expected phase ~0.5 at last quarter, got {phase}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_PhaseCycle_Increases_Then_Decreases()
|
||||
{
|
||||
// Check phase increases from new moon to full moon
|
||||
var startDate = new DateTime(2025, 1, 29, 12, 0, 0, DateTimeKind.Utc); // New moon
|
||||
double prevPhase = Lunar.CalculatePhase(startDate);
|
||||
|
||||
// Check for 7 days after new moon - phase should generally increase
|
||||
for (int day = 1; day <= 7; day++)
|
||||
{
|
||||
var date = startDate.AddDays(day);
|
||||
double phase = Lunar.CalculatePhase(date);
|
||||
|
||||
// Allow small fluctuations due to orbital mechanics
|
||||
Assert.True(phase >= prevPhase - 0.01,
|
||||
$"Phase should increase from new moon: day {day}, prev={prevPhase}, curr={phase}");
|
||||
prevPhase = phase;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_LunarMonth_Cycle()
|
||||
{
|
||||
// One lunar month is approximately 29.53 days
|
||||
var startDate = new DateTime(2025, 1, 29, 12, 36, 0, DateTimeKind.Utc); // New Moon
|
||||
double startPhase = Lunar.CalculatePhase(startDate);
|
||||
|
||||
// After ~29.53 days, should be back to similar phase
|
||||
var endDate = startDate.AddDays(29.53);
|
||||
double endPhase = Lunar.CalculatePhase(endDate);
|
||||
|
||||
Assert.True(Math.Abs(startPhase - endPhase) < 0.1,
|
||||
$"Phase should return to ~same value after lunar month: start={startPhase}, end={endPhase}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_Batch_MatchesStreaming()
|
||||
{
|
||||
var startDate = new DateTime(2025, 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] = Lunar.CalculatePhase(date);
|
||||
}
|
||||
|
||||
// Calculate using batch
|
||||
var output = new double[count];
|
||||
Lunar.Batch(timestamps, output);
|
||||
|
||||
// Compare
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(expected[i], output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_TSeries_Update()
|
||||
{
|
||||
var startDate = new DateTime(2025, 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 lunar = new Lunar();
|
||||
var result = lunar.Update(series);
|
||||
|
||||
Assert.Equal(30, result.Count);
|
||||
|
||||
// Verify each value
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double expectedPhase = Lunar.CalculatePhase(series[i].Time);
|
||||
Assert.Equal(expectedPhase, result[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_StaticCalculate_TSeries()
|
||||
{
|
||||
var startDate = new DateTime(2025, 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 = Lunar.Batch(series);
|
||||
|
||||
Assert.Equal(30, result.Count);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double expectedPhase = Lunar.CalculatePhase(series[i].Time);
|
||||
Assert.Equal(expectedPhase, result[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_Chaining_Works()
|
||||
{
|
||||
var source = new Sma(10);
|
||||
var lunar = new Lunar(source);
|
||||
|
||||
bool eventFired = false;
|
||||
lunar.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 Lunar_Reset()
|
||||
{
|
||||
var lunar = new Lunar();
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
lunar.Update(input);
|
||||
|
||||
lunar.Reset();
|
||||
|
||||
// After reset, Last should be reset
|
||||
Assert.Equal(0, lunar.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_UnixTimestamp_CalculatesCorrectly()
|
||||
{
|
||||
// Test using known Unix timestamp
|
||||
// January 1, 2020 00:00:00 UTC = 1577836800000 ms
|
||||
long unixMs = 1577836800000;
|
||||
double phase1 = Lunar.CalculatePhase(unixMs);
|
||||
|
||||
var dateTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
double phase2 = Lunar.CalculatePhase(dateTime);
|
||||
|
||||
Assert.Equal(phase1, phase2, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_Phase_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 phase = Lunar.CalculatePhase(date);
|
||||
|
||||
Assert.True(phase >= 0.0 && phase <= 1.0,
|
||||
$"Phase out of range at {date}: {phase}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_HistoricalNewMoon_1999()
|
||||
{
|
||||
// December 7, 1999 - New Moon at 22:32 UTC
|
||||
var newMoon = new DateTime(1999, 12, 7, 22, 32, 0, DateTimeKind.Utc);
|
||||
double phase = Lunar.CalculatePhase(newMoon);
|
||||
|
||||
Assert.True(phase < 0.05, $"Expected low phase at 1999 new moon, got {phase}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_HistoricalFullMoon_2000()
|
||||
{
|
||||
// January 21, 2000 - Full Moon (also a lunar eclipse)
|
||||
var fullMoon = new DateTime(2000, 1, 21, 4, 40, 0, DateTimeKind.Utc);
|
||||
double phase = Lunar.CalculatePhase(fullMoon);
|
||||
|
||||
Assert.True(phase > 0.95, $"Expected high phase at 2000 full moon, got {phase}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_Batch_ThrowsOnLengthMismatch()
|
||||
{
|
||||
var timestamps = new long[10];
|
||||
var output = new double[5];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Lunar.Batch(timestamps, output));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_EmptyTSeries_ReturnsEmpty()
|
||||
{
|
||||
var lunar = new Lunar();
|
||||
var empty = new TSeries();
|
||||
var result = lunar.Update(empty);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_IsNew_Parameter_DoesNotAffectResult()
|
||||
{
|
||||
var lunar = new Lunar();
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
|
||||
var result1 = lunar.Update(input, isNew: true);
|
||||
|
||||
lunar.Reset();
|
||||
|
||||
var result2 = lunar.Update(input, isNew: false);
|
||||
|
||||
// Lunar phase is deterministic from timestamp, isNew shouldn't matter
|
||||
Assert.Equal(result1.Value, result2.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lunar_DateTimeKind_Unspecified_TreatedAsUtc()
|
||||
{
|
||||
var unspecified = new DateTime(2025, 1, 15, 12, 0, 0, DateTimeKind.Unspecified);
|
||||
var utc = new DateTime(2025, 1, 15, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
double phase1 = Lunar.CalculatePhase(unspecified);
|
||||
double phase2 = Lunar.CalculatePhase(utc);
|
||||
|
||||
Assert.Equal(phase1, phase2, Tolerance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for Lunar Phase indicator.
|
||||
/// Lunar is a deterministic astronomical calculation not implemented in trading libraries
|
||||
/// (TA-Lib, Skender, Tulip), so validation is done against known astronomical events
|
||||
/// and mathematical properties of the lunar cycle.
|
||||
/// </summary>
|
||||
public class LunarValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Validation_OutputRange_ZeroToOne()
|
||||
{
|
||||
// Lunar phase output should always be in [0, 1]
|
||||
var lunar = new Lunar();
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
lunar.Update(new TValue(bar.Time, bar.Close));
|
||||
double val = lunar.Last.Value;
|
||||
Assert.True(val >= 0.0 && val <= 1.0,
|
||||
$"Lunar phase {val} is outside expected range [0, 1]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_DeterministicForSameTimestamp()
|
||||
{
|
||||
// Same timestamp should always produce the same lunar phase
|
||||
var lunar1 = new Lunar();
|
||||
var lunar2 = new Lunar();
|
||||
|
||||
var fixedTime = new DateTime(2024, 1, 15, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
lunar1.Update(new TValue(fixedTime, 100.0));
|
||||
lunar2.Update(new TValue(fixedTime, 200.0));
|
||||
|
||||
Assert.Equal(lunar1.Last.Value, lunar2.Last.Value, 1e-12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_PriceIndependent()
|
||||
{
|
||||
// Lunar phase depends only on timestamp, not on price
|
||||
var lunar = new Lunar();
|
||||
|
||||
var t1 = new DateTime(2024, 3, 10, 0, 0, 0, DateTimeKind.Utc);
|
||||
lunar.Update(new TValue(t1, 50.0));
|
||||
double val1 = lunar.Last.Value;
|
||||
|
||||
lunar = new Lunar();
|
||||
lunar.Update(new TValue(t1, 999.0));
|
||||
double val2 = lunar.Last.Value;
|
||||
|
||||
Assert.Equal(val1, val2, 1e-12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_CyclePeriodApprox29Days()
|
||||
{
|
||||
// The synodic lunar cycle is ~29.53 days
|
||||
// Over a 60-day window we should see roughly 2 full cycles
|
||||
var lunar = new Lunar();
|
||||
var start = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
var values = new List<double>();
|
||||
for (int day = 0; day < 60; day++)
|
||||
{
|
||||
var t = start.AddDays(day);
|
||||
lunar.Update(new TValue(t, 100.0));
|
||||
values.Add(lunar.Last.Value);
|
||||
}
|
||||
|
||||
// Verify the cycle completes: values should vary significantly over 60 days
|
||||
double minVal = values.Min();
|
||||
double maxVal = values.Max();
|
||||
double range = maxVal - minVal;
|
||||
|
||||
// Over 60 days (~2 synodic months) we should see significant variation
|
||||
Assert.True(range > 0.5,
|
||||
$"Expected lunar phase range > 0.5 over 60 days, got range={range} (min={minVal}, max={maxVal})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_FiniteOutputs()
|
||||
{
|
||||
// All outputs should be finite
|
||||
var lunar = new Lunar();
|
||||
|
||||
var gbm = new GBM(seed: 99);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
lunar.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.True(double.IsFinite(lunar.Last.Value),
|
||||
$"Lunar produced non-finite value: {lunar.Last.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user