package com.goldenchart.service; import com.fasterxml.jackson.databind.JsonNode; import java.util.List; import java.util.Map; /** * 보조지표 visual_config hline — 전략 DSL HL_OVER/HL_MID/HL_UNDER 역할 해석. */ public final class IndicatorHlineResolver { private IndicatorHlineResolver() {} public static Double resolveThresholdField( JsonNode cond, String field, String dslIndicatorType, Map> indicatorVisual) { if (field == null || field.isBlank()) return null; boolean override = cond != null && cond.path("thresholdOverride").asBoolean(false); if (override) { if (cond.has("targetValue") && !cond.path("targetValue").isNull()) { return cond.path("targetValue").asDouble(); } if (field.startsWith("K_")) { try { return Double.parseDouble(field.substring(2)); } catch (NumberFormatException ignored) {} } return legacyNumericField(field); } if ("HL_OVER".equals(field) || "HL_MID".equals(field) || "HL_UNDER".equals(field)) { Double fromVisual = fromHlineRole(field, dslIndicatorType, indicatorVisual); if (fromVisual != null) return fromVisual; } if (field.startsWith("K_")) { Double fromVisual = inferRoleFromLegacyK(field, dslIndicatorType, indicatorVisual); if (fromVisual != null) return fromVisual; try { return Double.parseDouble(field.substring(2)); } catch (NumberFormatException ignored) {} } return legacyNumericField(field); } private static Double fromHlineRole( String role, String dslIndicatorType, Map> indicatorVisual) { String registryKey = StrategyDslToTa4jAdapter.registryKeyForDsl(dslIndicatorType); if (indicatorVisual == null || registryKey == null) return defaultForRole(dslIndicatorType, role); Map visual = indicatorVisual.get(registryKey); if (visual == null) return defaultForRole(dslIndicatorType, role); @SuppressWarnings("unchecked") List> hlines = (List>) visual.get("hlines"); if (hlines == null || hlines.isEmpty()) return defaultForRole(dslIndicatorType, role); String label = switch (role) { case "HL_OVER" -> "과열선"; case "HL_MID" -> "중앙선"; case "HL_UNDER" -> "침체선"; default -> null; }; if (label == null) return null; for (Map h : hlines) { Object lbl = h.get("label"); if (lbl != null && label.equals(lbl.toString())) { Object price = h.get("price"); if (price instanceof Number n) return n.doubleValue(); } } // 기준선 폴백 (Disparity 등) if ("HL_MID".equals(role)) { for (Map h : hlines) { Object lbl = h.get("label"); if (lbl != null && "기준선".equals(lbl.toString())) { Object price = h.get("price"); if (price instanceof Number n) return n.doubleValue(); } } } return defaultForRole(dslIndicatorType, role); } private static Double inferRoleFromLegacyK( String kField, String dslIndicatorType, Map> indicatorVisual) { try { double val = Double.parseDouble(kField.substring(2)); for (String role : List.of("HL_OVER", "HL_MID", "HL_UNDER")) { Double roleVal = fromHlineRole(role, dslIndicatorType, indicatorVisual); if (roleVal != null && Math.abs(roleVal - val) < 0.0001) { return roleVal; } } } catch (NumberFormatException ignored) {} return null; } private static Double legacyNumericField(String field) { if (field.contains("_NEG")) { String[] parts = field.split("_NEG"); try { return -Double.parseDouble(parts[parts.length - 1]); } catch (NumberFormatException ignored) {} } if ("ZERO_LINE".equals(field)) return 0.0; int idx = field.lastIndexOf('_'); if (idx >= 0 && idx < field.length() - 1) { try { return Double.parseDouble(field.substring(idx + 1)); } catch (NumberFormatException ignored) {} } return null; } private static Double defaultForRole(String dslIndicatorType, String role) { return switch (dslIndicatorType + ":" + role) { case "RSI:HL_OVER" -> 70.0; case "RSI:HL_MID" -> 50.0; case "RSI:HL_UNDER" -> 30.0; case "CCI:HL_OVER" -> 100.0; case "CCI:HL_MID" -> 0.0; case "CCI:HL_UNDER" -> -100.0; case "STOCHASTIC:HL_OVER" -> 80.0; case "STOCHASTIC:HL_MID" -> 50.0; case "STOCHASTIC:HL_UNDER" -> 20.0; case "ADX:HL_MID" -> 25.0; case "WILLIAMS_R:HL_OVER" -> -20.0; case "DISPARITY:HL_MID" -> 100.0; case "VOLUME_OSC:HL_MID" -> 0.0; case "TRIX:HL_MID" -> 0.0; default -> null; }; } }