using System; using System.Collections.Generic; namespace QuantEngine.Core.Domain { public class SellPriceSanityResult { public string SellPriceSanityStatus { get; set; } = "PASS"; public List SellPriceSanityIssues { get; set; } = new List(); public bool HtsAllowed { get; set; } = true; public bool ShadowLedger { get; set; } public string Ticker { get; set; } = string.Empty; } public static class SellPriceSanityChecker { public static SellPriceSanityResult CheckSellPriceSanity( double sellLimitPrice, double? stopLossPrice, double prevClose, string ticker = "") { var issues = new List(); string status = "PASS"; if (stopLossPrice.HasValue && sellLimitPrice < stopLossPrice.Value) { issues.Add($"INVALID_PRICE_INVERSION: sell={sellLimitPrice:N0} < stop={stopLossPrice.Value:N0}"); status = "INVALID_PRICE_INVERSION"; } double upperLimit = prevClose * 1.30; if (sellLimitPrice > upperLimit) { issues.Add($"INVALID_UNREALISTIC_PRICE: sell={sellLimitPrice:N0} > prev_close*1.30={upperLimit:N0}"); if (status == "PASS") { status = "INVALID_UNREALISTIC_PRICE"; } } int tickUnit = KrxTickNormalizer.GetTickUnit(sellLimitPrice); if (sellLimitPrice % tickUnit != 0) { double corrected = KrxTickNormalizer.NormalizeTick(sellLimitPrice); issues.Add($"INVALID_TICK: sell={sellLimitPrice:N0} 호가단위={tickUnit}원 → 정규화={corrected:N0}"); if (status == "PASS") { status = "INVALID_TICK"; } } return new SellPriceSanityResult { SellPriceSanityStatus = status, SellPriceSanityIssues = issues, HtsAllowed = status == "PASS", ShadowLedger = status != "PASS", Ticker = ticker }; } } }