diff --git a/src/dotnet/QuantEngine.Core.Tests/FactorCalculatorTests.cs b/src/dotnet/QuantEngine.Core.Tests/FactorCalculatorTests.cs index 5bc6cbfb..f3193dd5 100644 --- a/src/dotnet/QuantEngine.Core.Tests/FactorCalculatorTests.cs +++ b/src/dotnet/QuantEngine.Core.Tests/FactorCalculatorTests.cs @@ -125,5 +125,28 @@ namespace QuantEngine.Core.Tests Assert.Equal(expectedMom20, outputs.Momentum20D, 5); Assert.Equal(1.0, outputs.Beta60D); } + + [Fact] + public void CalculateFactors_DuplicateDates_UsesLastOccurrencePerDate() + { + var stock = CreateMockBars("005930", 100.0, 1.0, 130); + var index = CreateMockBars("KOSPI", 2000.0, 0.0, 130); + + var duplicateDate = stock[129].TradeDate; + stock.Add(new PriceHistoryDailyRecord( + "005930", + duplicateDate, + 1000m, + 1002m, + 998m, + 1001m, + 100000, + "TEST_SOURCE")); + + var outputs = FactorCalculator.CalculateFactors(stock, index); + + Assert.True(outputs.Momentum20D > 0); + Assert.True(outputs.Atr20Pct > 0); + } } } diff --git a/src/dotnet/QuantEngine.Core/Domain/FactorCalculator.cs b/src/dotnet/QuantEngine.Core/Domain/FactorCalculator.cs index c1e9d866..4190f0d7 100644 --- a/src/dotnet/QuantEngine.Core/Domain/FactorCalculator.cs +++ b/src/dotnet/QuantEngine.Core/Domain/FactorCalculator.cs @@ -26,8 +26,8 @@ namespace QuantEngine.Core.Domain return new FactorOutputs(0, 0, 0, 0, 0, 1.0, 0); } - var sortedStock = stockBars.OrderBy(b => b.TradeDate).ToList(); - var sortedIndex = indexBars?.OrderBy(b => b.TradeDate).ToList() ?? new List(); + var sortedStock = NormalizeBars(stockBars); + var sortedIndex = NormalizeBars(indexBars); return new FactorOutputs( CalculateMomentum(sortedStock, 20), CalculateMomentum(sortedStock, 60), @@ -38,6 +38,20 @@ namespace QuantEngine.Core.Domain CalculateRs20D(sortedStock, sortedIndex)); } + private static List NormalizeBars(List? bars) + { + if (bars == null || bars.Count == 0) + { + return new List(); + } + + return bars + .OrderBy(b => b.TradeDate) + .GroupBy(b => b.TradeDate) + .Select(g => g.Last()) + .ToList(); + } + private static double CalculateMomentum(List bars, int period) { if (bars.Count <= period) return 0.0;