137 lines
5.0 KiB
Java
137 lines
5.0 KiB
Java
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<String, Double> 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<String, Double> 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;
|
|
}
|
|
}
|
|
}
|