From 090752f42c8fb4605165778d28a2d6629d13d635 Mon Sep 17 00:00:00 2001 From: Macbook Date: Thu, 11 Jun 2026 08:28:05 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=EC=97=94=EB=93=9C=20=EC=A0=84?= =?UTF-8?q?=EB=9E=B5=20=EC=A1=B0=EA=B1=B4=20=ED=8F=89=EA=B0=80=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=203=EA=B1=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. evaluateOverallRule: 5m 전략 평가 시 1m 고정 시리즈 사용 버그 수정 - DSL에서 주 시간봉(TIMEFRAME 노드 candleType)을 추출하여 해당 시리즈 사용 - 없으면 1m 폴백 유지 2. buildConditionRule: kLength 등 지표 파라미터를 조건 노드 params에서 우선 읽도록 수정 - 조건 DSL의 params 필드가 있으면 전역 indicator settings보다 우선 적용 - 서버 DB에 indicator settings 없어도 전략 저장 당시 값 사용 가능 3. LiveConditionController: deviceId 헤더를 indicatorSettingsService에 전달 - x-device-id 헤더를 읽어 device 단위 지표 설정 조회 지원 Co-authored-by: Cursor --- .../controller/LiveConditionController.java | 3 +- .../service/LiveConditionStatusService.java | 44 +++++++++++++++-- .../service/StrategyDslToTa4jAdapter.java | 49 +++++++++++++++++-- 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/com/goldenchart/controller/LiveConditionController.java b/backend/src/main/java/com/goldenchart/controller/LiveConditionController.java index e96a1d7..5ac6fb8 100644 --- a/backend/src/main/java/com/goldenchart/controller/LiveConditionController.java +++ b/backend/src/main/java/com/goldenchart/controller/LiveConditionController.java @@ -26,6 +26,7 @@ public class LiveConditionController { @RequestParam long strategyId, @RequestHeader Map headers) { long uid = TradingControllerSupport.requireRegisteredUser(headers); - return ResponseEntity.ok(service.evaluate(uid, null, market, strategyId)); + String deviceId = headers.get("x-device-id"); + return ResponseEntity.ok(service.evaluate(uid, deviceId, market, strategyId)); } } diff --git a/backend/src/main/java/com/goldenchart/service/LiveConditionStatusService.java b/backend/src/main/java/com/goldenchart/service/LiveConditionStatusService.java index 423abaa..3cf9eda 100644 --- a/backend/src/main/java/com/goldenchart/service/LiveConditionStatusService.java +++ b/backend/src/main/java/com/goldenchart/service/LiveConditionStatusService.java @@ -174,9 +174,15 @@ public class LiveConditionStatusService { objectMapper.readTree(conditionJson); if (dsl == null || dsl.isNull()) return null; - // 기준 시리즈: 1m 봉 - if (!ta4jStorage.exists(market, "1m")) return null; - BarSeries series = ta4jStorage.getOrCreate(market, "1m"); + // DSL 루트의 주 시간봉 추출 (TIMEFRAME 노드가 있으면 해당 봉 사용) + // 5m 전략에서 1m 시리즈로 평가하면 CROSS_UP 등이 부정확해지는 버그 수정 + String primaryTf = extractPrimaryTimeframe(dsl); + if (!ta4jStorage.exists(market, primaryTf)) { + // 지정 봉이 없으면 1m으로 폴백 + primaryTf = "1m"; + if (!ta4jStorage.exists(market, "1m")) return null; + } + BarSeries series = ta4jStorage.getOrCreate(market, primaryTf); if (series.isEmpty()) return null; StrategyDslToTa4jAdapter.RuleBuildContext ctx = @@ -190,6 +196,38 @@ public class LiveConditionStatusService { } } + /** + * DSL 트리에서 주 시간봉을 추출한다. + * TIMEFRAME 노드가 있으면 그 candleType을 반환, 없으면 "1m". + * 복수의 TIMEFRAME이 있으면 가장 길게 등장하는 봉 우선 (다수결). + */ + private String extractPrimaryTimeframe(com.fasterxml.jackson.databind.JsonNode dsl) { + Map tfCount = new java.util.LinkedHashMap<>(); + collectTimeframes(dsl, tfCount); + if (tfCount.isEmpty()) return "1m"; + return tfCount.entrySet().stream() + .max(java.util.Map.Entry.comparingByValue()) + .map(java.util.Map.Entry::getKey) + .orElse("1m"); + } + + private void collectTimeframes(com.fasterxml.jackson.databind.JsonNode node, + Map out) { + if (node == null || node.isNull()) return; + String type = node.path("type").asText("CONDITION"); + if ("TIMEFRAME".equals(type)) { + String tf = LiveStrategyTimeframeService.normalize( + node.path("candleType").asText("1m")); + out.merge(tf, 1, Integer::sum); + } + com.fasterxml.jackson.databind.JsonNode children = node.path("children"); + if (children.isArray()) { + for (com.fasterxml.jackson.databind.JsonNode c : children) collectTimeframes(c, out); + } + com.fasterxml.jackson.databind.JsonNode child = node.path("child"); + if (!child.isMissingNode()) collectTimeframes(child, out); + } + private void collectConditions(String json, String timeframe, String side, List out) { if (json == null || json.isBlank()) return; diff --git a/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java b/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java index 4ec067d..3fc91a2 100644 --- a/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java +++ b/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java @@ -427,9 +427,14 @@ public class StrategyDslToTa4jAdapter { // DSL 타입(STOCHASTIC 등) → DB 레지스트리 키(Stochastic 등) 변환 후 파라미터 조회 String registryKey = toRegistryKey(indType); - Map indParams = params != null + Map globalParams = params != null ? params.getOrDefault(registryKey, Map.of()) : Map.of(); + // 조건 노드에 직접 저장된 params가 있으면 전역 설정보다 우선 적용 + // 예) {"kLength": 12, "smooth": 3} → 전역 Stochastic 설정(kLength=14)을 덮어씀 + Map condNodeParams = extractConditionNodeParams(cond); + Map indParams = condNodeParams.isEmpty() ? globalParams + : mergeParams(globalParams, condNodeParams); try { Indicator left = resolveField(leftField, indType, indParams, series, condPeriod, leftPeriod, cond, ctx); @@ -1217,12 +1222,50 @@ public class StrategyDslToTa4jAdapter { private int intP(Map p, String k, int def) { if (p == null) return def; Object v = p.get(k); - return v == null ? def : ((Number) v).intValue(); + if (v == null) return def; + if (v instanceof Number n) return n.intValue(); + try { return Integer.parseInt(v.toString()); } catch (NumberFormatException e) { return def; } } private double dblP(Map p, String k, double def) { if (p == null) return def; Object v = p.get(k); - return v == null ? def : ((Number) v).doubleValue(); + if (v == null) return def; + if (v instanceof Number n) return n.doubleValue(); + try { return Double.parseDouble(v.toString()); } catch (NumberFormatException e) { return def; } + } + + /** + * 조건 노드 JSON에서 직접 저장된 파라미터를 추출한다. + * 전략 편집기가 저장한 조건별 override params (예: {"kLength": 12}). + * 없거나 객체가 아니면 빈 맵 반환. + */ + @SuppressWarnings("unchecked") + private Map extractConditionNodeParams(JsonNode cond) { + if (cond == null || cond.isNull()) return Map.of(); + JsonNode paramsNode = cond.path("params"); + if (paramsNode.isMissingNode() || paramsNode.isNull() || !paramsNode.isObject()) { + return Map.of(); + } + try { + Map result = new java.util.LinkedHashMap<>(); + paramsNode.fields().forEachRemaining(e -> { + com.fasterxml.jackson.databind.JsonNode v = e.getValue(); + if (v.isNumber()) result.put(e.getKey(), v.numberValue()); + else if (v.isTextual()) result.put(e.getKey(), v.textValue()); + else if (v.isBoolean()) result.put(e.getKey(), v.booleanValue()); + }); + return result; + } catch (Exception ex) { + return Map.of(); + } + } + + /** globalParams 위에 overrides를 덮어쓴 새 맵 반환 */ + private Map mergeParams(Map global, + Map overrides) { + Map merged = new java.util.LinkedHashMap<>(global); + merged.putAll(overrides); + return merged; } }