직접입력값 평가오류

This commit is contained in:
Macbook
2026-06-17 23:45:39 +09:00
parent 3130de113b
commit d4b5814bbc
7 changed files with 220 additions and 24 deletions
@@ -222,14 +222,8 @@ public class LiveConditionStatusService {
Map<String, Map<String, Object>> params,
Map<String, Map<String, Object>> visual) {
try {
JsonNode buyDslRaw = timeframeNormalizer.normalize(
objectMapper.readTree(
strategy.getBuyConditionJson() != null ? strategy.getBuyConditionJson() : "null"),
strategy.getName());
JsonNode sellDslRaw = timeframeNormalizer.normalize(
objectMapper.readTree(
strategy.getSellConditionJson() != null ? strategy.getSellConditionJson() : "null"),
strategy.getName());
JsonNode buyDslRaw = loadStrategyDsl(strategy.getBuyConditionJson(), strategy.getName());
JsonNode sellDslRaw = loadStrategyDsl(strategy.getSellConditionJson(), strategy.getName());
String primaryTf = OhlcvBarSeriesSupport.normalizeTf(chartTimeframe);
JsonNode buyDsl = timeframeNormalizer.remapForChartTimeframe(buyDslRaw, primaryTf);
@@ -396,8 +390,9 @@ public class LiveConditionStatusService {
Long barTimeSec) {
if (conditionJson == null || conditionJson.isBlank()) return null;
try {
String normalized = StrategyConditionThresholdNormalizer.normalizeJson(conditionJson, objectMapper);
com.fasterxml.jackson.databind.JsonNode dsl =
objectMapper.readTree(conditionJson);
objectMapper.readTree(normalized);
if (dsl == null || dsl.isNull()) return null;
// DSL 루트의 주 시간봉 추출 (TIMEFRAME 노드가 있으면 해당 봉 사용)
@@ -458,12 +453,22 @@ public class LiveConditionStatusService {
List<PendingCond> out) {
if (json == null || json.isBlank()) return;
try {
walk(objectMapper.readTree(json), timeframe, side, out, new HashSet<>());
String normalized = StrategyConditionThresholdNormalizer.normalizeJson(json, objectMapper);
walk(objectMapper.readTree(normalized), timeframe, side, out, new HashSet<>());
} catch (Exception e) {
log.warn("[LiveCondition] JSON walk fail: {}", e.getMessage());
}
}
private JsonNode loadStrategyDsl(String json, String strategyName) throws Exception {
JsonNode root = (json == null || json.isBlank())
? objectMapper.readTree("null")
: objectMapper.readTree(json);
return timeframeNormalizer.normalize(
StrategyConditionThresholdNormalizer.normalizeTree(root),
strategyName);
}
private void walk(JsonNode node, String timeframe, String side,
List<PendingCond> out, Set<String> seen) {
if (node == null || node.isNull()) return;
@@ -718,14 +723,8 @@ public class LiveConditionStatusService {
try {
String strategyName = strategy.getName();
JsonNode buyDslRaw = timeframeNormalizer.normalize(
objectMapper.readTree(
strategy.getBuyConditionJson() != null ? strategy.getBuyConditionJson() : "null"),
strategyName);
JsonNode sellDslRaw = timeframeNormalizer.normalize(
objectMapper.readTree(
strategy.getSellConditionJson() != null ? strategy.getSellConditionJson() : "null"),
strategyName);
JsonNode buyDslRaw = loadStrategyDsl(strategy.getBuyConditionJson(), strategyName);
JsonNode sellDslRaw = loadStrategyDsl(strategy.getSellConditionJson(), strategyName);
String primaryTf = OhlcvBarSeriesSupport.normalizeTf(chartTimeframe);
JsonNode buyDsl = timeframeNormalizer.remapForChartTimeframe(buyDslRaw, primaryTf);
@@ -0,0 +1,129 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Map;
/**
* 전략 조건 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);
}
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) {
normalizeThresholdField(cond, "rightField");
normalizeThresholdField(cond, "leftField");
}
private static void normalizeThresholdField(ObjectNode cond, String fieldKey) {
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(cond.path("indicatorType").asText(""), 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(cond.path("indicatorType").asText(""), 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;
}
}
}
@@ -66,11 +66,15 @@ public class StrategyService {
}
entity.setBuyConditionJson(
buyJson != null
? dslTimeframeNormalizer.normalizeJson(buyJson, strategyName)
? dslTimeframeNormalizer.normalizeJson(
StrategyConditionThresholdNormalizer.normalizeJson(buyJson, objectMapper),
strategyName)
: null);
entity.setSellConditionJson(
sellJson != null
? dslTimeframeNormalizer.normalizeJson(sellJson, strategyName)
? dslTimeframeNormalizer.normalizeJson(
StrategyConditionThresholdNormalizer.normalizeJson(sellJson, objectMapper),
strategyName)
: null);
if (dto.getFlowLayout() != null && !dto.getFlowLayout().isNull()) {
entity.setFlowLayoutJson(objectMapper.writeValueAsString(dto.getFlowLayout()));
@@ -0,0 +1,39 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class StrategyConditionThresholdNormalizerTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
@Test
void normalizesK50OverrideToHlMid() throws Exception {
JsonNode root = MAPPER.readTree("""
{
"type": "CONDITION",
"condition": {
"indicatorType": "RSI",
"conditionType": "CROSS_UP",
"leftField": "RSI_VALUE_9",
"rightField": "K_50",
"period": 9,
"valuePeriodOverride": true,
"thresholdOverride": true,
"targetValue": 50
}
}
""");
JsonNode normalized = StrategyConditionThresholdNormalizer.normalizeTree(root);
JsonNode cond = normalized.path("condition");
assertEquals("HL_MID", cond.path("rightField").asText());
assertFalse(cond.path("thresholdOverride").asBoolean(true));
assertFalse(cond.has("targetValue"));
}
}