211 lines
8.6 KiB
Java
211 lines
8.6 KiB
Java
package com.goldenchart.service;
|
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
import lombok.RequiredArgsConstructor;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
import java.util.LinkedHashSet;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
import java.util.regex.Pattern;
|
|
|
|
/**
|
|
* 전략 DSL에 TIMEFRAME 래핑이 없으면 평가 분봉을 추론해 래핑한다.
|
|
* (프론트 encodeConditionForSave 와 동일 목적)
|
|
*/
|
|
@Component
|
|
@RequiredArgsConstructor
|
|
public class StrategyDslTimeframeNormalizer {
|
|
|
|
private static final Pattern NAME_3M = Pattern.compile("3\\s*분|3m", Pattern.CASE_INSENSITIVE);
|
|
private static final Pattern NAME_5M = Pattern.compile("5\\s*분|5m", Pattern.CASE_INSENSITIVE);
|
|
private static final Pattern NAME_10M = Pattern.compile("10\\s*분|10m", Pattern.CASE_INSENSITIVE);
|
|
private static final Pattern NAME_15M = Pattern.compile("15\\s*분|15m", Pattern.CASE_INSENSITIVE);
|
|
private static final Pattern NAME_30M = Pattern.compile("30\\s*분|30m", Pattern.CASE_INSENSITIVE);
|
|
private static final Pattern NAME_1H = Pattern.compile("1\\s*시간|1h", Pattern.CASE_INSENSITIVE);
|
|
private static final Pattern NAME_4H = Pattern.compile("4\\s*시간|4h", Pattern.CASE_INSENSITIVE);
|
|
private static final Pattern NAME_1D = Pattern.compile("일봉|1\\s*일|1d", Pattern.CASE_INSENSITIVE);
|
|
|
|
private final ObjectMapper objectMapper;
|
|
|
|
public JsonNode normalize(JsonNode root) {
|
|
return normalize(root, null);
|
|
}
|
|
|
|
public JsonNode normalize(JsonNode root, String strategyName) {
|
|
if (root == null || root.isNull()) return root;
|
|
|
|
if ("TIMEFRAME".equals(root.path("type").asText(""))) {
|
|
// START 다중 분봉(candleTypes) — 편집기 저장값 유지
|
|
if (hasMultipleCandleTypes(root)) {
|
|
return root;
|
|
}
|
|
String expected = inferPrimaryCandleType(root, strategyName);
|
|
String current = LiveStrategyTimeframeService.normalize(root.path("candleType").asText("1m"));
|
|
if (!expected.equals(current)) {
|
|
return copyTimeframeWithCandleType(root, expected);
|
|
}
|
|
return root;
|
|
}
|
|
|
|
if (hasTimeframeScope(root)) return root;
|
|
|
|
String primary = inferPrimaryCandleType(root, strategyName);
|
|
ObjectNode wrapped = objectMapper.createObjectNode();
|
|
wrapped.put("id", UUID.randomUUID().toString());
|
|
wrapped.put("type", "TIMEFRAME");
|
|
wrapped.put("candleType", primary);
|
|
ArrayNode children = objectMapper.createArrayNode();
|
|
children.add(root);
|
|
wrapped.set("children", children);
|
|
return wrapped;
|
|
}
|
|
|
|
public String normalizeJson(String json) {
|
|
return normalizeJson(json, null);
|
|
}
|
|
|
|
public String normalizeJson(String json, String strategyName) {
|
|
if (json == null || json.isBlank()) return json;
|
|
try {
|
|
JsonNode root = objectMapper.readTree(json);
|
|
JsonNode normalized = normalize(root, strategyName);
|
|
return objectMapper.writeValueAsString(normalized);
|
|
} catch (Exception e) {
|
|
return json;
|
|
}
|
|
}
|
|
|
|
/** 전략 이름에서 평가 분봉 추론 (예: cci_3분봉 테스트 → 3m) */
|
|
public static String inferFromStrategyName(String name) {
|
|
if (name == null || name.isBlank()) return null;
|
|
if (NAME_3M.matcher(name).find()) return "3m";
|
|
if (NAME_5M.matcher(name).find()) return "5m";
|
|
if (NAME_10M.matcher(name).find()) return "10m";
|
|
if (NAME_15M.matcher(name).find()) return "15m";
|
|
if (NAME_30M.matcher(name).find()) return "30m";
|
|
if (NAME_4H.matcher(name).find()) return "4h";
|
|
if (NAME_1H.matcher(name).find()) return "1h";
|
|
if (NAME_1D.matcher(name).find()) return "1d";
|
|
return null;
|
|
}
|
|
|
|
/** 루트 TIMEFRAME 또는 AND|OR + 전부 TIMEFRAME 자식 */
|
|
public static boolean hasTimeframeScope(JsonNode node) {
|
|
if (node == null || node.isNull()) return false;
|
|
String type = node.path("type").asText("");
|
|
if ("TIMEFRAME".equals(type)) return true;
|
|
if ("AND".equals(type) || "OR".equals(type)) {
|
|
JsonNode children = node.path("children");
|
|
if (children.isArray() && !children.isEmpty()) {
|
|
for (JsonNode c : children) {
|
|
if (!"TIMEFRAME".equals(c.path("type").asText(""))) return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return findNestedTimeframe(node) != null;
|
|
}
|
|
|
|
private static JsonNode findNestedTimeframe(JsonNode node) {
|
|
if (node == null || node.isNull()) return null;
|
|
if ("TIMEFRAME".equals(node.path("type").asText(""))) return node;
|
|
JsonNode children = node.path("children");
|
|
if (children.isArray()) {
|
|
for (JsonNode c : children) {
|
|
JsonNode found = findNestedTimeframe(c);
|
|
if (found != null) return found;
|
|
}
|
|
}
|
|
JsonNode child = node.path("child");
|
|
if (!child.isMissingNode() && !child.isNull()) return findNestedTimeframe(child);
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 트리에서 명시적 분봉 수집 — CONDITION 의 left/rightCandleType, TIMEFRAME 노드.
|
|
*/
|
|
public static Set<String> collectExplicitCandleTypes(JsonNode node) {
|
|
Set<String> out = new LinkedHashSet<>();
|
|
collectExplicitCandleTypes(node, out);
|
|
return out;
|
|
}
|
|
|
|
/** 조건 기본값 1m 은 제외하고 의미 있는 분봉만 */
|
|
public static Set<String> collectMeaningfulCandleTypes(JsonNode node) {
|
|
Set<String> all = collectExplicitCandleTypes(node);
|
|
Set<String> meaningful = new LinkedHashSet<>();
|
|
for (String ct : all) {
|
|
if (!"1m".equals(ct)) meaningful.add(ct);
|
|
}
|
|
return meaningful;
|
|
}
|
|
|
|
private static void collectExplicitCandleTypes(JsonNode node, Set<String> out) {
|
|
if (node == null || node.isNull()) return;
|
|
|
|
String type = node.path("type").asText("");
|
|
if ("TIMEFRAME".equals(type)) {
|
|
StrategyConditionTimeframeService.addTimeframeCandleTypes(node, out);
|
|
}
|
|
|
|
if ("CONDITION".equals(type)) {
|
|
JsonNode cond = node.path("condition");
|
|
if (!cond.isMissingNode() && !cond.isNull()) {
|
|
if (cond.has("leftCandleType") && !cond.path("leftCandleType").asText("").isBlank()) {
|
|
out.add(LiveStrategyTimeframeService.normalize(cond.path("leftCandleType").asText()));
|
|
}
|
|
if (cond.has("rightCandleType") && !cond.path("rightCandleType").asText("").isBlank()) {
|
|
out.add(LiveStrategyTimeframeService.normalize(cond.path("rightCandleType").asText()));
|
|
}
|
|
}
|
|
}
|
|
|
|
JsonNode children = node.path("children");
|
|
if (children.isArray()) {
|
|
for (JsonNode c : children) collectExplicitCandleTypes(c, out);
|
|
}
|
|
JsonNode child = node.path("child");
|
|
if (!child.isMissingNode() && !child.isNull()) collectExplicitCandleTypes(child, out);
|
|
}
|
|
|
|
public static String inferPrimaryCandleType(JsonNode node) {
|
|
return inferPrimaryCandleType(node, null);
|
|
}
|
|
|
|
public static String inferPrimaryCandleType(JsonNode node, String strategyName) {
|
|
JsonNode nested = findNestedTimeframe(node);
|
|
if (nested != null) {
|
|
String ct = LiveStrategyTimeframeService.normalize(nested.path("candleType").asText("1m"));
|
|
if (!"1m".equals(ct)) return ct;
|
|
String fromName = inferFromStrategyName(strategyName);
|
|
if (fromName != null) return fromName;
|
|
return ct;
|
|
}
|
|
|
|
Set<String> meaningful = collectMeaningfulCandleTypes(node);
|
|
if (!meaningful.isEmpty()) return meaningful.iterator().next();
|
|
|
|
String fromName = inferFromStrategyName(strategyName);
|
|
if (fromName != null) return fromName;
|
|
|
|
Set<String> explicit = collectExplicitCandleTypes(node);
|
|
if (explicit.size() == 1) return explicit.iterator().next();
|
|
return "1m";
|
|
}
|
|
|
|
private static boolean hasMultipleCandleTypes(JsonNode timeframeNode) {
|
|
JsonNode arr = timeframeNode.path("candleTypes");
|
|
return arr.isArray() && arr.size() > 1;
|
|
}
|
|
|
|
private ObjectNode copyTimeframeWithCandleType(JsonNode tf, String candleType) {
|
|
ObjectNode copy = tf.deepCopy();
|
|
copy.put("candleType", LiveStrategyTimeframeService.normalize(candleType));
|
|
return copy;
|
|
}
|
|
}
|