using System.Globalization;
namespace Infrastructure.StockData;
///
/// Stooq 라이트 배치 시세(/q/l/?f=sd2t2ohlc&h&e=csv) CSV 파서 — 순수 C#.
/// 헤더: Symbol,Date,Time,Open,High,Low,Close. 값 결측은 "N/D" 로 온다(장 시작 전·심볼 오류 등).
/// Close 가 유효하지 않은 행은 버리고, Open/High/Low 결측은 Close 로 보정한다.
///
public static class WorldIndexCsvParser
{
/// 세계지수 시세 1행 (외부 소스 최신 종가 기준)
public sealed record WorldIndexQuote(
string Symbol,
DateOnly TradeDate,
decimal Open,
decimal High,
decimal Low,
decimal Close
);
public static IReadOnlyList Parse(string csv)
{
var rows = new List();
if (string.IsNullOrWhiteSpace(csv))
{
return rows;
}
var lines = csv.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var line in lines)
{
var cols = line.Split(',');
// 헤더 행(Symbol,Date,...) 및 컬럼 부족 행 skip
if (cols.Length < 7 || cols[0].Trim().Equals("Symbol", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var symbol = cols[0].Trim();
var tradeDate = ParseDate(cols[1]);
var close = ParseDecimal(cols[6]);
if (symbol.Length == 0 || tradeDate is null || close is null)
{
continue;
}
// OHLC 결측(N/D)은 종가로 보정
var open = ParseDecimal(cols[3]) ?? close.Value;
var high = ParseDecimal(cols[4]) ?? close.Value;
var low = ParseDecimal(cols[5]) ?? close.Value;
rows.Add(new WorldIndexQuote(symbol.ToLowerInvariant(), tradeDate.Value, open, high, low, close.Value));
}
return rows;
}
private static DateOnly? ParseDate(string raw)
{
return DateOnly.TryParseExact(raw.Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null;
}
/// Stooq 수치 파싱 — "N/D"·빈값·파싱 실패는 null.
private static decimal? ParseDecimal(string raw)
{
return decimal.TryParse(raw.Trim(), NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null;
}
}