package com.goldenchart.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.Map; import java.util.Locale; /** * 전략 조건 DSL — K_50 등 직접입력 스냅샷을 hline 역할(HL_MID 등)로 정규화. * 프론트 normalizeConditionForPersistence 와 동일 의도. */ public final class StrategyConditionThresholdNormalizer { private StrategyConditionThresholdNormalizer() {} public static JsonNode normalizeTree(JsonNode root) { if (root == null || root.isNull() || !root.isObject()) return root; ObjectNode copy = root.deepCopy(); walk(copy); return copy; } private static void walk(JsonNode node) { if (node == null || !node.isObject()) return; JsonNode cond = node.get("condition"); if (cond != null && cond.isObject()) { normalizeCondition((ObjectNode) cond); } else if (node.has("indicatorType") && node.has("conditionType")) { normalizeCondition((ObjectNode) node); } JsonNode children = node.get("children"); if (children != null && children.isArray()) { for (JsonNode c : children) walk(c); } JsonNode child = node.get("child"); if (child != null) walk(child); } private static void normalizeCondition(ObjectNode cond) { String indType = cond.path("indicatorType").asText("").toUpperCase(Locale.ROOT); if (!indType.isBlank()) { cond.put("indicatorType", indType); } normalizeThresholdField(cond, "rightField", indType); normalizeThresholdField(cond, "leftField", indType); } private static void normalizeThresholdField(ObjectNode cond, String fieldKey, String indType) { String field = cond.path(fieldKey).asText(""); if (field.isBlank()) return; boolean override = cond.path("thresholdOverride").asBoolean(false); Double value = readThresholdValue(cond, field); if (override || field.startsWith("K_")) { String role = inferRoleForValue(indType, value, field); if (role != null) { cond.put(fieldKey, role); cond.remove("targetValue"); cond.put("thresholdOverride", false); return; } } if (!override && field.startsWith("K_")) { String role = inferRoleForValue(indType, value, field); if (role != null) { cond.put(fieldKey, role); cond.remove("targetValue"); cond.put("thresholdOverride", false); } } } private static Double readThresholdValue(ObjectNode cond, String field) { 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 null; } } return null; } private static String inferRoleForValue(String indicatorType, Double value, String field) { if (value == null && field.startsWith("K_")) { try { value = Double.parseDouble(field.substring(2)); } catch (NumberFormatException ignored) { return null; } } if (value == null || !Double.isFinite(value)) return null; double eps = 0.0001; for (Map.Entry e : defaultRoles(indicatorType).entrySet()) { if (Math.abs(e.getValue() - value) < eps) { return e.getKey(); } } return null; } /** HL_OVER / HL_MID / HL_UNDER → 기본 숫자 (IndicatorHlineResolver.defaultForRole 과 동일) */ private static Map defaultRoles(String indicatorType) { return switch (indicatorType) { case "RSI" -> Map.of("HL_OVER", 70.0, "HL_MID", 50.0, "HL_UNDER", 30.0); case "STOCHASTIC" -> Map.of("HL_OVER", 80.0, "HL_MID", 50.0, "HL_UNDER", 20.0); case "CCI" -> Map.of("HL_OVER", 100.0, "HL_MID", 0.0, "HL_UNDER", -100.0); case "WILLIAMS_R" -> Map.of("HL_OVER", -20.0, "HL_MID", -50.0, "HL_UNDER", -80.0); case "ADX" -> Map.of("HL_MID", 25.0); case "DISPARITY" -> Map.of("HL_MID", 100.0); case "VOLUME_OSC", "TRIX", "MACD" -> Map.of("HL_MID", 0.0); default -> Map.of(); }; } /** JSON 문자열 보정 — 파싱 실패 시 원본 반환 */ public static String normalizeJson(String json, com.fasterxml.jackson.databind.ObjectMapper mapper) { if (json == null || json.isBlank()) return json; try { JsonNode root = mapper.readTree(json); JsonNode normalized = normalizeTree(root); return mapper.writeValueAsString(normalized); } catch (Exception ignored) { return json; } } }