using System.Globalization; namespace Infrastructure.StockData; /// /// Stooq 라이트 배치 시세(/q/l/?f=sd2t2ohlcv&h&e=csv) CSV 파서 — 순수 C#. WorldIndexCsvParser 의 거래량(Volume) 확장판. /// 헤더: Symbol,Date,Time,Open,High,Low,Close,Volume. 값 결측은 "N/D" 로 온다(장 시작 전·심볼 오류 등). /// Close 가 유효하지 않은 행은 버리고, Open/High/Low 결측은 Close 로, Volume 결측은 0 으로 보정한다. /// public static class MarketQuoteCsvParser { /// 주요 종목·상품 시세 1행 (외부 소스 최신 종가 기준) public sealed record MarketQuote( string Symbol, DateOnly TradeDate, decimal Open, decimal High, decimal Low, decimal Close, long Volume ); 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 (Volume 은 선택 — 7컬럼도 허용) 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)은 종가로, Volume 결측·미제공은 0 으로 보정 var open = ParseDecimal(cols[3]) ?? close.Value; var high = ParseDecimal(cols[4]) ?? close.Value; var low = ParseDecimal(cols[5]) ?? close.Value; var volume = cols.Length >= 8 ? ParseLong(cols[7]) : 0L; rows.Add(new MarketQuote(symbol.ToLowerInvariant(), tradeDate.Value, open, high, low, close.Value, volume)); } 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; } private static decimal? ParseDecimal(string raw) { return decimal.TryParse(raw.Trim(), NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : null; } private static long ParseLong(string raw) { return long.TryParse(raw.Trim(), NumberStyles.Integer | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value) ? value : 0L; } }