refactor(dotnet): normalize factor input dates
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Successful in 19s
Validators (Pushes and Pull Requests) / validate-core (push) Successful in 2m6s

This commit is contained in:
2026-07-13 00:20:46 +09:00
parent 29929d76d3
commit 6475ecd3b0
2 changed files with 39 additions and 2 deletions
@@ -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);
}
}
}
@@ -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<PriceHistoryDailyRecord>();
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<PriceHistoryDailyRecord> NormalizeBars(List<PriceHistoryDailyRecord>? bars)
{
if (bars == null || bars.Count == 0)
{
return new List<PriceHistoryDailyRecord>();
}
return bars
.OrderBy(b => b.TradeDate)
.GroupBy(b => b.TradeDate)
.Select(g => g.Last())
.ToList();
}
private static double CalculateMomentum(List<PriceHistoryDailyRecord> bars, int period)
{
if (bars.Count <= period) return 0.0;