알림문제 수정
This commit is contained in:
+3
-9
@@ -235,16 +235,10 @@ public class StrategyConditionTimeframeService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** TIMEFRAME 노드 — candleTypes 배열 또는 단일 candleType */
|
/** TIMEFRAME 노드 — candleTypes 배열 또는 단일 candleType (stale 1m 제거 반영) */
|
||||||
static void addTimeframeCandleTypes(JsonNode timeframeNode, Set<String> out) {
|
static void addTimeframeCandleTypes(JsonNode timeframeNode, Set<String> out) {
|
||||||
JsonNode arr = timeframeNode.path("candleTypes");
|
for (String ct : StrategyDslTimeframeNormalizer.resolveStartCandleTypes(timeframeNode)) {
|
||||||
if (arr.isArray() && !arr.isEmpty()) {
|
out.add(ct);
|
||||||
for (JsonNode t : arr) {
|
|
||||||
String ct = LiveStrategyTimeframeService.normalize(t.asText(""));
|
|
||||||
if (!ct.isBlank()) out.add(ct);
|
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
out.add(LiveStrategyTimeframeService.normalize(timeframeNode.path("candleType").asText("1m")));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package com.goldenchart.service;
|
|||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -39,9 +42,9 @@ public class StrategyDslTimeframeNormalizer {
|
|||||||
if (root == null || root.isNull()) return root;
|
if (root == null || root.isNull()) return root;
|
||||||
|
|
||||||
if ("TIMEFRAME".equals(root.path("type").asText(""))) {
|
if ("TIMEFRAME".equals(root.path("type").asText(""))) {
|
||||||
// START 다중 분봉(candleTypes) — 편집기 저장값 유지, candleType 은 배열 첫 값과 동기화
|
// START 다중 분봉(candleTypes) — stale 1m 제거 후 candleType 동기화
|
||||||
if (hasCandleTypesArray(root)) {
|
if (hasCandleTypesArray(root)) {
|
||||||
return syncTimeframePrimary(root);
|
return syncTimeframePrimary(reconcileStartCandleTypes(root));
|
||||||
}
|
}
|
||||||
String current = LiveStrategyTimeframeService.normalize(root.path("candleType").asText("1m"));
|
String current = LiveStrategyTimeframeService.normalize(root.path("candleType").asText("1m"));
|
||||||
// 편집기에서 명시한 단일 분봉(3m, 5m 등)은 조건 노드 기본 1m 추론으로 덮어쓰지 않음
|
// 편집기에서 명시한 단일 분봉(3m, 5m 등)은 조건 노드 기본 1m 추론으로 덮어쓰지 않음
|
||||||
@@ -270,6 +273,71 @@ public class StrategyDslTimeframeNormalizer {
|
|||||||
return arr.isArray() && !arr.isEmpty();
|
return arr.isArray() && !arr.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* START candleTypes 에 남은 레거시 1m 제거.
|
||||||
|
* <ul>
|
||||||
|
* <li>[1m, 5m] + 조건 leftCandleType 5m → [5m] (편집기 5m만 선택·1m 기본값 잔존)</li>
|
||||||
|
* <li>[1m, 3m, 5m] 다중 OR 선택은 유지</li>
|
||||||
|
* <li>조건에 1m 명시 시 유지</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public static Set<String> resolveStartCandleTypes(JsonNode timeframeNode) {
|
||||||
|
ObjectNode reconciled = reconcileStartCandleTypesStatic(timeframeNode);
|
||||||
|
Set<String> out = new LinkedHashSet<>();
|
||||||
|
JsonNode arr = reconciled.path("candleTypes");
|
||||||
|
if (arr.isArray() && !arr.isEmpty()) {
|
||||||
|
for (JsonNode t : arr) {
|
||||||
|
String ct = LiveStrategyTimeframeService.normalize(t.asText(""));
|
||||||
|
if (!ct.isBlank()) out.add(ct);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
out.add(LiveStrategyTimeframeService.normalize(
|
||||||
|
reconciled.path("candleType").asText("1m")));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectNode reconcileStartCandleTypes(JsonNode tf) {
|
||||||
|
return reconcileStartCandleTypesStatic(tf);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ObjectNode reconcileStartCandleTypesStatic(JsonNode tf) {
|
||||||
|
ObjectNode copy = tf.deepCopy();
|
||||||
|
JsonNode arr = tf.path("candleTypes");
|
||||||
|
if (!arr.isArray() || arr.size() < 2) return copy;
|
||||||
|
|
||||||
|
JsonNode subtree = firstTimeframeChild(tf);
|
||||||
|
Set<String> explicit = collectExplicitCandleTypes(subtree);
|
||||||
|
if (explicit.contains("1m")) return copy;
|
||||||
|
|
||||||
|
List<String> normalized = new ArrayList<>();
|
||||||
|
for (JsonNode t : arr) {
|
||||||
|
String ct = LiveStrategyTimeframeService.normalize(t.asText(""));
|
||||||
|
if (!ct.isBlank() && !normalized.contains(ct)) normalized.add(ct);
|
||||||
|
}
|
||||||
|
if (!normalized.contains("1m")) return copy;
|
||||||
|
|
||||||
|
long non1mCount = normalized.stream().filter(ct -> !"1m".equals(ct)).count();
|
||||||
|
if (non1mCount == 0) return copy;
|
||||||
|
// [1m, 3m, 5m] 등 1m+복수 상위봉 OR — 1m 의도적 선택으로 간주
|
||||||
|
if (non1mCount >= 2) return copy;
|
||||||
|
|
||||||
|
ArrayNode newArr = JsonNodeFactory.instance.arrayNode();
|
||||||
|
for (String ct : normalized) {
|
||||||
|
if (!"1m".equals(ct)) newArr.add(ct);
|
||||||
|
}
|
||||||
|
if (newArr.isEmpty()) return copy;
|
||||||
|
copy.set("candleTypes", newArr);
|
||||||
|
copy.put("candleType", LiveStrategyTimeframeService.normalize(newArr.get(0).asText("1m")));
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonNode firstTimeframeChild(JsonNode timeframeNode) {
|
||||||
|
JsonNode children = timeframeNode.path("children");
|
||||||
|
if (children.isArray() && !children.isEmpty()) return children.get(0);
|
||||||
|
return timeframeNode.path("child");
|
||||||
|
}
|
||||||
|
|
||||||
/** candleTypes[] 가 있으면 candleType 을 첫 항목과 맞춤 (stale 1m 방지) */
|
/** candleTypes[] 가 있으면 candleType 을 첫 항목과 맞춤 (stale 1m 방지) */
|
||||||
private ObjectNode syncTimeframePrimary(JsonNode tf) {
|
private ObjectNode syncTimeframePrimary(JsonNode tf) {
|
||||||
ObjectNode copy = tf.deepCopy();
|
ObjectNode copy = tf.deepCopy();
|
||||||
|
|||||||
@@ -57,15 +57,20 @@ public class StrategyService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
String strategyName = entity.getName() != null ? entity.getName() : dto.getName();
|
String strategyName = entity.getName() != null ? entity.getName() : dto.getName();
|
||||||
|
String buyJson = dto.getBuyCondition() != null
|
||||||
|
? objectMapper.writeValueAsString(dto.getBuyCondition()) : null;
|
||||||
|
String sellJson = dto.getSellCondition() != null
|
||||||
|
? objectMapper.writeValueAsString(dto.getSellCondition()) : null;
|
||||||
|
if (buyJson != null && sellJson != null) {
|
||||||
|
sellJson = dslTimeframeNormalizer.alignSellStartTimeframeToBuy(buyJson, sellJson);
|
||||||
|
}
|
||||||
entity.setBuyConditionJson(
|
entity.setBuyConditionJson(
|
||||||
dto.getBuyCondition() != null
|
buyJson != null
|
||||||
? dslTimeframeNormalizer.normalizeJson(
|
? dslTimeframeNormalizer.normalizeJson(buyJson, strategyName)
|
||||||
objectMapper.writeValueAsString(dto.getBuyCondition()), strategyName)
|
|
||||||
: null);
|
: null);
|
||||||
entity.setSellConditionJson(
|
entity.setSellConditionJson(
|
||||||
dto.getSellCondition() != null
|
sellJson != null
|
||||||
? dslTimeframeNormalizer.normalizeJson(
|
? dslTimeframeNormalizer.normalizeJson(sellJson, strategyName)
|
||||||
objectMapper.writeValueAsString(dto.getSellCondition()), strategyName)
|
|
||||||
: null);
|
: null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("전략 DSL 직렬화 실패: {}", e.getMessage());
|
log.warn("전략 DSL 직렬화 실패: {}", e.getMessage());
|
||||||
@@ -93,9 +98,19 @@ public class StrategyService {
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (entity.getSellConditionJson() != null && !entity.getSellConditionJson().isBlank()) {
|
String sellJson = entity.getSellConditionJson();
|
||||||
String fixed = dslTimeframeNormalizer.normalizeJson(
|
if (entity.getBuyConditionJson() != null && !entity.getBuyConditionJson().isBlank()
|
||||||
entity.getSellConditionJson(), entity.getName());
|
&& sellJson != null && !sellJson.isBlank()) {
|
||||||
|
String aligned = dslTimeframeNormalizer.alignSellStartTimeframeToBuy(
|
||||||
|
entity.getBuyConditionJson(), sellJson);
|
||||||
|
if (!aligned.equals(sellJson)) {
|
||||||
|
entity.setSellConditionJson(aligned);
|
||||||
|
sellJson = aligned;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sellJson != null && !sellJson.isBlank()) {
|
||||||
|
String fixed = dslTimeframeNormalizer.normalizeJson(sellJson, entity.getName());
|
||||||
if (!fixed.equals(entity.getSellConditionJson())) {
|
if (!fixed.equals(entity.getSellConditionJson())) {
|
||||||
entity.setSellConditionJson(fixed);
|
entity.setSellConditionJson(fixed);
|
||||||
changed = true;
|
changed = true;
|
||||||
|
|||||||
@@ -163,17 +163,7 @@ public class StrategyTriggerBranchEvaluator {
|
|||||||
|
|
||||||
/** START 다중 분봉 — 각 마감봉마다 동일 조건 트리를 독립 평가 */
|
/** START 다중 분봉 — 각 마감봉마다 동일 조건 트리를 독립 평가 */
|
||||||
private static List<String> readTimeframeCandleTypes(JsonNode timeframeNode) {
|
private static List<String> readTimeframeCandleTypes(JsonNode timeframeNode) {
|
||||||
JsonNode arr = timeframeNode.path("candleTypes");
|
return new ArrayList<>(StrategyDslTimeframeNormalizer.resolveStartCandleTypes(timeframeNode));
|
||||||
if (arr.isArray() && !arr.isEmpty()) {
|
|
||||||
List<String> list = new ArrayList<>();
|
|
||||||
for (JsonNode t : arr) {
|
|
||||||
String ct = LiveStrategyTimeframeService.normalize(t.asText(""));
|
|
||||||
if (!ct.isBlank() && !list.contains(ct)) list.add(ct);
|
|
||||||
}
|
|
||||||
if (!list.isEmpty()) return list;
|
|
||||||
}
|
|
||||||
return List.of(LiveStrategyTimeframeService.normalize(
|
|
||||||
timeframeNode.path("candleType").asText("1m")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class TriggerBranchRule implements Rule {
|
private final class TriggerBranchRule implements Rule {
|
||||||
|
|||||||
+36
@@ -159,6 +159,42 @@ class StrategyConditionTimeframeServiceTest {
|
|||||||
assertFalse(service.usesTimeframe(5L, "1m"));
|
assertFalse(service.usesTimeframe(5L, "1m"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void collectForStrategy_stale1m5mArrayUses5mOnlyWhenConditionsSay5m() {
|
||||||
|
GcStrategy strategy = new GcStrategy();
|
||||||
|
strategy.setId(6L);
|
||||||
|
strategy.setName("알트레이어 매수 알림");
|
||||||
|
strategy.setBuyConditionJson("""
|
||||||
|
{
|
||||||
|
"type": "TIMEFRAME",
|
||||||
|
"candleType": "1m",
|
||||||
|
"candleTypes": ["1m", "5m"],
|
||||||
|
"children": [{
|
||||||
|
"type": "CONDITION",
|
||||||
|
"condition": {
|
||||||
|
"indicatorType": "CCI",
|
||||||
|
"leftCandleType": "5m",
|
||||||
|
"rightCandleType": "5m"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
strategy.setSellConditionJson(null);
|
||||||
|
when(strategyRepo.findById(6L)).thenReturn(Optional.of(strategy));
|
||||||
|
when(dslNormalizer.alignSellStartTimeframeToBuy(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any()))
|
||||||
|
.thenAnswer(inv -> inv.getArgument(1));
|
||||||
|
when(dslNormalizer.normalizeJson(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any()))
|
||||||
|
.thenAnswer(inv -> {
|
||||||
|
String json = inv.getArgument(0);
|
||||||
|
String name = inv.getArgument(1);
|
||||||
|
return new StrategyDslTimeframeNormalizer(objectMapper).normalizeJson(json, name);
|
||||||
|
});
|
||||||
|
|
||||||
|
Set<String> tfs = service.collectForStrategy(6L);
|
||||||
|
assertEquals(Set.of("5m"), tfs);
|
||||||
|
assertFalse(service.usesTimeframe(6L, "1m"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void collectForStrategy_legacyTreeDefaultsTo1m() {
|
void collectForStrategy_legacyTreeDefaultsTo1m() {
|
||||||
GcStrategy legacy = new GcStrategy();
|
GcStrategy legacy = new GcStrategy();
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
class StrategyDslTimeframeNormalizerTest {
|
class StrategyDslTimeframeNormalizerTest {
|
||||||
@@ -49,6 +51,50 @@ class StrategyDslTimeframeNormalizerTest {
|
|||||||
assertEquals("3m", root.path("candleType").asText());
|
assertEquals("3m", root.path("candleType").asText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void normalize_dropsStale1mWhenConditionsUse5mOnly() throws Exception {
|
||||||
|
String json = """
|
||||||
|
{
|
||||||
|
"type": "TIMEFRAME",
|
||||||
|
"candleType": "1m",
|
||||||
|
"candleTypes": ["1m", "5m"],
|
||||||
|
"children": [{
|
||||||
|
"type": "CONDITION",
|
||||||
|
"condition": {
|
||||||
|
"indicatorType": "CCI",
|
||||||
|
"leftCandleType": "5m",
|
||||||
|
"rightCandleType": "5m"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
String normalized = normalizer.normalizeJson(json, "알트레이어 매수 알림");
|
||||||
|
JsonNode root = objectMapper.readTree(normalized);
|
||||||
|
assertEquals(1, root.path("candleTypes").size());
|
||||||
|
assertEquals("5m", root.path("candleTypes").get(0).asText());
|
||||||
|
assertEquals("5m", root.path("candleType").asText());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveStartCandleTypes_dropsStale1mPair() throws Exception {
|
||||||
|
String json = """
|
||||||
|
{
|
||||||
|
"type": "TIMEFRAME",
|
||||||
|
"candleType": "1m",
|
||||||
|
"candleTypes": ["1m", "5m"],
|
||||||
|
"children": [{
|
||||||
|
"type": "CONDITION",
|
||||||
|
"condition": {
|
||||||
|
"indicatorType": "CCI",
|
||||||
|
"leftCandleType": "5m"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
JsonNode root = objectMapper.readTree(json);
|
||||||
|
assertEquals(Set.of("5m"), StrategyDslTimeframeNormalizer.resolveStartCandleTypes(root));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void normalize_preservesMultiCandleTypesOnSave() throws Exception {
|
void normalize_preservesMultiCandleTypesOnSave() throws Exception {
|
||||||
String json = """
|
String json = """
|
||||||
|
|||||||
Reference in New Issue
Block a user