using System.Globalization; namespace QuantEngine.Core.Domain; /// /// Canonical .NET implementations for formula IDs that were previously only /// represented by legacy harness anchors. Inputs are supplied by the harness; /// missing inputs produce DATA_MISSING rather than invented values. /// public static class FormulaCanonicalCoverage { public static Dictionary AntiChaseV1(IReadOnlyDictionary input) { var velocity = Number(input, "velocity_1d"); var threshold = Number(input, "velocity_threshold"); if (!velocity.HasValue || !threshold.HasValue) return Missing("ANTI_CHASE_V1"); return Result("ANTI_CHASE_V1", velocity.Value > threshold.Value ? "BLOCK" : "PASS", velocity.Value); } public static Dictionary CashRecoveryV1(IReadOnlyDictionary input) { var shortfall = Number(input, "cash_shortfall_krw"); var recovered = Number(input, "recovered_krw"); if (!shortfall.HasValue || !recovered.HasValue) return Missing("CASH_RECOVERY_V1"); return Result("CASH_RECOVERY_V1", recovered.Value >= shortfall.Value ? "PASS" : "LIMITED", recovered.Value); } public static Dictionary ComprehensiveProposalV1(IReadOnlyDictionary input) => GateFromInputs("COMPREHENSIVE_PROPOSAL_V1", input, "proposal_gate"); public static Dictionary DfgV1(IReadOnlyDictionary input) => GateFromInputs("DFG_V1", input, "cycle_detected", invert: true); public static Dictionary IntradayV1(IReadOnlyDictionary input) => GateFromInputs("INTRADAY_V1", input, "intraday_restriction_gate"); public static Dictionary PortfolioHealthV1(IReadOnlyDictionary input) => GateFromInputs("PORTFOLIO_HEALTH_V1", input, "portfolio_health_label"); public static Dictionary RsV2Fusion(IReadOnlyDictionary input) { var rs = Number(input, "rs_v2_score"); var technical = Number(input, "technical_score"); if (!rs.HasValue || !technical.HasValue) return Missing("RS_V2_FUSION"); var score = (rs.Value + technical.Value) / 2d; return Result("RS_V2_FUSION", score >= 0 ? "PASS" : "BLOCK", score); } public static Dictionary StopBreachV1(IReadOnlyDictionary input) { var current = Number(input, "current_price"); var stop = Number(input, "stop_loss_price"); var gap = Number(input, "gap_threshold"); if (!current.HasValue || !stop.HasValue || !gap.HasValue || stop.Value == 0) return Missing("STOP_BREACH_V1"); var gapPct = (stop.Value - current.Value) / stop.Value; return Result("STOP_BREACH_V1", gapPct >= gap.Value ? "BREACH_IMMEDIATE_EXIT" : "PASS", gapPct); } public static Dictionary TickNormV1(IReadOnlyDictionary input) { var price = Number(input, "price"); if (!price.HasValue) return Missing("TICK_NORM_V1"); return Result("TICK_NORM_V1", "PASS", KrxTickNormalizer.NormalizeTick(price.Value)); } private static Dictionary GateFromInputs(string id, IReadOnlyDictionary input, string field, bool invert = false) { if (!input.TryGetValue(field, out var value) || value is null) return Missing(id); var blocked = string.Equals(value.ToString(), "BLOCK", StringComparison.OrdinalIgnoreCase) || string.Equals(value.ToString(), "true", StringComparison.OrdinalIgnoreCase); if (invert) blocked = !blocked; return Result(id, blocked ? "BLOCK" : "PASS", null); } private static Dictionary Missing(string id) => new() { ["formula_id"] = id, ["gate"] = "DATA_MISSING — 하네스 업데이트 필요", ["value"] = null, }; private static Dictionary Result(string id, string gate, double? value) => new() { ["formula_id"] = id, ["gate"] = gate, ["value"] = value, }; private static double? Number(IReadOnlyDictionary input, string key) { if (!input.TryGetValue(key, out var value) || value is null) return null; return double.TryParse(Convert.ToString(value, CultureInfo.InvariantCulture), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) ? parsed : null; } }