using QuantEngine.Core.Models;
namespace QuantEngine.Application.Services;
///
/// 가격 데이터 정규화 — Python _collect_one() 로직 대응
///
public class PriceDataNormalizer
{
private readonly SourcePriorityResolver _priorityResolver;
public PriceDataNormalizer(SourcePriorityResolver priorityResolver)
{
_priorityResolver = priorityResolver;
}
public (Dictionary Normalized, Dictionary Provenance) NormalizeCollectionRow(
Dictionary row,
PriceSourceResult? kis,
PriceSourceResult? naver,
bool includeNaver = false)
{
var ticker = (row.GetValueOrDefault("Ticker") as string) ?? (row.GetValueOrDefault("ticker") as string) ?? "";
var name = (row.GetValueOrDefault("Name") as string) ?? (row.GetValueOrDefault("name") as string) ?? "";
var sector = (row.GetValueOrDefault("Sector") as string) ?? (row.GetValueOrDefault("sector") as string);
var normalized = new Dictionary(row);
var (sourcePriority, provenance) = _priorityResolver.ResolveSourcePriority(
ticker, kis, naver, includeNaver: includeNaver);
// KIS 데이터 병합
if (kis?.Status == "OK")
{
MergeSourceFields(normalized, kis, new[] { "current_price", "open", "high", "low", "volume" });
MergeSourceFields(normalized, kis, new[] { "relative_return_20d", "volume_ratio_5d", "microstructure_pressure", "short_turnover_share" });
}
// Naver 폴백
if (naver?.Status == "OK" || naver?.Status == "DATA_MISSING")
{
// Removed
// Removed
NormalizedSetDefault(normalized, "naver_price_status", naver?.Status);
NormalizedSetDefault(normalized, "current_price", naver?.CurrentPrice);
NormalizedSetDefault(normalized, "open", naver?.Open);
NormalizedSetDefault(normalized, "high", naver?.High);
NormalizedSetDefault(normalized, "low", naver?.Low);
NormalizedSetDefault(normalized, "volume", naver?.Volume);
}
// 최종 폴백 (기초 데이터)
NormalizedSetDefault(normalized, "current_price", DataNormalizationHelper.CoerceFloat(row.GetValueOrDefault("current_price") ?? row.GetValueOrDefault("Current_Price") ?? row.GetValueOrDefault("close")));
NormalizedSetDefault(normalized, "open", DataNormalizationHelper.CoerceFloat(row.GetValueOrDefault("open") ?? row.GetValueOrDefault("Open")));
NormalizedSetDefault(normalized, "high", DataNormalizationHelper.CoerceFloat(row.GetValueOrDefault("high") ?? row.GetValueOrDefault("High")));
NormalizedSetDefault(normalized, "low", DataNormalizationHelper.CoerceFloat(row.GetValueOrDefault("low") ?? row.GetValueOrDefault("Low")));
NormalizedSetDefault(normalized, "volume", DataNormalizationHelper.CoerceFloat(row.GetValueOrDefault("volume") ?? row.GetValueOrDefault("Volume")));
normalized["collection_as_of"] = DataNormalizationHelper.KstNowIso();
return (normalized, provenance);
}
private void MergeSourceFields(Dictionary target, PriceSourceResult source, string[] keys)
{
foreach (var key in keys)
{
var value = source.GetType().GetProperty(ToPascalCase(key))?.GetValue(source);
if (value != null && !string.IsNullOrEmpty(value.ToString()))
target[key] = value;
}
}
private void NormalizedSetDefault(Dictionary normalized, string key, object? value)
{
if (!normalized.ContainsKey(key) && value != null && !string.IsNullOrEmpty(value.ToString()))
normalized[key] = value;
}
private string ToPascalCase(string snake)
{
return System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(snake.Replace("_", " ")).Replace(" ", "");
}
}