package com.goldenchart.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.goldenchart.entity.GcStrategy; import com.goldenchart.repository.GcStrategyRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * 전략 DSL(buy/sell LogicNode)에 포함된 조건 판별 시간봉 수집. * START / TIMEFRAME / AND|OR+TIMEFRAME 분기와 동일 규칙. */ @Service @RequiredArgsConstructor @Slf4j public class StrategyConditionTimeframeService { private final GcStrategyRepository strategyRepo; private final ObjectMapper objectMapper; private final ConcurrentHashMap> cache = new ConcurrentHashMap<>(); public Set collectForStrategy(long strategyId) { return cache.computeIfAbsent(strategyId, this::loadFromDb); } public List collectForStrategyList(long strategyId) { return List.copyOf(collectForStrategy(strategyId)); } public boolean usesTimeframe(long strategyId, String candleType) { String ct = LiveStrategyTimeframeService.normalize(candleType); return collectForStrategy(strategyId).contains(ct); } public void invalidate(long strategyId) { cache.remove(strategyId); } public void invalidateAll() { cache.clear(); } private Set loadFromDb(long strategyId) { Optional opt = strategyRepo.findById(strategyId); if (opt.isEmpty()) return Set.of("1m"); GcStrategy strategy = opt.get(); Set out = new LinkedHashSet<>(); collectFromJson(strategy.getBuyConditionJson(), out); collectFromJson(strategy.getSellConditionJson(), out); return out.isEmpty() ? Set.of("1m") : out; } private void collectFromJson(String json, Set out) { if (json == null || json.isBlank()) return; try { collectFromNode(objectMapper.readTree(json), out); } catch (Exception e) { log.warn("[StrategyTimeframes] JSON 파싱 실패: {}", e.getMessage()); out.add("1m"); } } private void collectFromNode(JsonNode node, Set out) { if (node == null || node.isNull()) return; String type = node.path("type").asText(""); if ("TIMEFRAME".equals(type)) { out.add(LiveStrategyTimeframeService.normalize(node.path("candleType").asText("1m"))); return; } if ("AND".equals(type) || "OR".equals(type)) { JsonNode children = node.path("children"); if (children.isArray() && !children.isEmpty()) { if (allTimeframeChildren(children)) { for (JsonNode tf : children) { out.add(LiveStrategyTimeframeService.normalize(tf.path("candleType").asText("1m"))); } return; } for (JsonNode child : children) { collectFromNode(child, out); } return; } } JsonNode child = node.path("child"); if (!child.isMissingNode() && !child.isNull()) { collectFromNode(child, out); return; } // 단일 START(기본 1m) 또는 TIMEFRAME 래핑 없는 트리 out.add("1m"); } private static boolean allTimeframeChildren(JsonNode children) { for (JsonNode c : children) { if (!"TIMEFRAME".equals(c.path("type").asText(""))) return false; } return true; } }