1분봉 오류 수정

This commit is contained in:
Macbook
2026-05-29 15:56:43 +09:00
parent e497d03925
commit a5f0a7eefd
4 changed files with 136 additions and 17 deletions
@@ -103,12 +103,21 @@ public class StrategyConditionTimeframeService {
}
/**
* START/flow 에만 남은 stale 1m 제거 — DSL 조건에 명시된 분봉 기준.
* START/flow 에만 남은 stale 1m 제거 — CONDITION 에 선언된 분봉 기준(기본값 1m 제외).
*/
private Set<String> sanitizeEvaluationTimeframes(Set<String> scope, GcStrategy strategy) {
Set<String> explicit = collectExplicitFromStrategy(strategy);
if (explicit.isEmpty()) return scope;
if (!explicit.contains("1m") && scope.contains("1m")) {
if (!scope.contains("1m") || scope.size() <= 1) return scope;
Set<String> conditionDeclared = collectConditionDeclaredFromStrategy(strategy);
if (conditionDeclared.contains("1m") && conditionDeclared.size() > 1) {
return scope;
}
if (!conditionDeclared.contains("1m")) {
Set<String> filtered = new LinkedHashSet<>(scope);
filtered.remove("1m");
return filtered.isEmpty() ? scope : filtered;
}
if (conditionDeclared.equals(Set.of("1m"))) {
Set<String> filtered = new LinkedHashSet<>(scope);
filtered.remove("1m");
return filtered.isEmpty() ? scope : filtered;
@@ -116,19 +125,20 @@ public class StrategyConditionTimeframeService {
return scope;
}
private Set<String> collectExplicitFromStrategy(GcStrategy strategy) {
private Set<String> collectConditionDeclaredFromStrategy(GcStrategy strategy) {
Set<String> out = new LinkedHashSet<>();
collectExplicitFromJson(strategy.getBuyConditionJson(), out);
collectExplicitFromJson(strategy.getSellConditionJson(), out);
collectConditionDeclaredFromJson(strategy.getBuyConditionJson(), out);
collectConditionDeclaredFromJson(strategy.getSellConditionJson(), out);
return out;
}
private void collectExplicitFromJson(String json, Set<String> out) {
private void collectConditionDeclaredFromJson(String json, Set<String> out) {
if (json == null || json.isBlank()) return;
try {
out.addAll(StrategyDslTimeframeNormalizer.collectExplicitCandleTypes(objectMapper.readTree(json)));
out.addAll(StrategyDslTimeframeNormalizer.collectConditionDeclaredCandleTypes(
objectMapper.readTree(json)));
} catch (Exception e) {
log.warn("[StrategyTimeframes] explicit JSON 파싱 실패: {}", e.getMessage());
log.warn("[StrategyTimeframes] condition-declared JSON 파싱 실패: {}", e.getMessage());
}
}
@@ -208,6 +208,36 @@ public class StrategyDslTimeframeNormalizer {
return meaningful;
}
/** CONDITION 노드에 선언된 left/rightCandleType 만 — TIMEFRAME·기본 1m 래핑 제외 */
public static Set<String> collectConditionDeclaredCandleTypes(JsonNode node) {
Set<String> out = new LinkedHashSet<>();
collectConditionDeclaredCandleTypes(node, out);
return out;
}
private static void collectConditionDeclaredCandleTypes(JsonNode node, Set<String> out) {
if (node == null || node.isNull()) return;
if ("CONDITION".equals(node.path("type").asText(""))) {
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) collectConditionDeclaredCandleTypes(c, out);
}
JsonNode child = node.path("child");
if (!child.isMissingNode() && !child.isNull()) collectConditionDeclaredCandleTypes(child, out);
}
private static void collectExplicitCandleTypes(JsonNode node, Set<String> out) {
if (node == null || node.isNull()) return;
@@ -314,14 +344,29 @@ public class StrategyDslTimeframeNormalizer {
return fromConditions;
}
/** START 에만 남은 stale 1m 제거 — 조건에 1m 이 없으면 평가·알림 분봉에서 제외 */
/**
* START 에만 남은 stale 1m 제거.
* 조건 노드 기본값 leftCandleType=1m 만 있을 때는 1m 을 평가 분봉으로 보지 않음.
* 의도적 다중 OR(조건에 1m·3m 등 복수 선언)은 유지.
*/
private static Set<String> dropStale1mFromStart(Set<String> fromStart, Set<String> fromConditions) {
if (!fromStart.contains("1m") || fromConditions.contains("1m")) {
if (!fromStart.contains("1m") || !hasExplicitNon1mStart(fromStart)) {
return fromStart;
}
Set<String> filtered = new LinkedHashSet<>(fromStart);
filtered.remove("1m");
return filtered.isEmpty() ? fromStart : filtered;
if (fromConditions.size() > 1 && fromConditions.contains("1m")) {
return fromStart;
}
if (fromConditions.size() == 1 && fromConditions.contains("1m")) {
Set<String> filtered = new LinkedHashSet<>(fromStart);
filtered.remove("1m");
return filtered.isEmpty() ? fromStart : filtered;
}
if (!fromConditions.contains("1m")) {
Set<String> filtered = new LinkedHashSet<>(fromStart);
filtered.remove("1m");
return filtered.isEmpty() ? fromStart : filtered;
}
return fromStart;
}
private ObjectNode writeResolvedTimeframe(JsonNode root, String strategyName) {
@@ -368,8 +413,8 @@ public class StrategyDslTimeframeNormalizer {
if (!arr.isArray() || arr.size() < 2) return copy;
JsonNode subtree = firstTimeframeChild(tf);
Set<String> explicit = collectExplicitCandleTypes(subtree);
if (explicit.contains("1m")) return copy;
Set<String> conditionDeclared = collectConditionDeclaredCandleTypes(subtree);
if (conditionDeclared.contains("1m")) return copy;
List<String> normalized = new ArrayList<>();
for (JsonNode t : arr) {