전략편집기 수정, 보조지표 설정 수정

This commit is contained in:
Macbook
2026-05-27 23:59:30 +09:00
parent 8cc0d1c88c
commit 2713b2951d
17 changed files with 210 additions and 46 deletions
@@ -39,12 +39,16 @@ public class StrategyDslTimeframeNormalizer {
if (root == null || root.isNull()) return root;
if ("TIMEFRAME".equals(root.path("type").asText(""))) {
// START 다중 분봉(candleTypes) — 편집기 저장값 유지
if (hasMultipleCandleTypes(root)) {
// START 다중 분봉(candleTypes) — 편집기 저장값 유지, candleType 은 배열 첫 값과 동기화
if (hasCandleTypesArray(root)) {
return syncTimeframePrimary(root);
}
String current = LiveStrategyTimeframeService.normalize(root.path("candleType").asText("1m"));
// 편집기에서 명시한 단일 분봉(3m, 5m 등)은 조건 노드 기본 1m 추론으로 덮어쓰지 않음
if (!"1m".equals(current)) {
return root;
}
String expected = inferPrimaryCandleType(root, strategyName);
String current = LiveStrategyTimeframeService.normalize(root.path("candleType").asText("1m"));
if (!expected.equals(current)) {
return copyTimeframeWithCandleType(root, expected);
}
@@ -197,9 +201,20 @@ public class StrategyDslTimeframeNormalizer {
return "1m";
}
private static boolean hasMultipleCandleTypes(JsonNode timeframeNode) {
private static boolean hasCandleTypesArray(JsonNode timeframeNode) {
JsonNode arr = timeframeNode.path("candleTypes");
return arr.isArray() && arr.size() > 1;
return arr.isArray() && !arr.isEmpty();
}
/** candleTypes[] 가 있으면 candleType 을 첫 항목과 맞춤 (stale 1m 방지) */
private ObjectNode syncTimeframePrimary(JsonNode tf) {
ObjectNode copy = tf.deepCopy();
JsonNode arr = tf.path("candleTypes");
if (arr.isArray() && !arr.isEmpty()) {
String primary = LiveStrategyTimeframeService.normalize(arr.get(0).asText("1m"));
copy.put("candleType", primary);
}
return copy;
}
private ObjectNode copyTimeframeWithCandleType(JsonNode tf, String candleType) {
@@ -55,6 +55,30 @@ class StrategyConditionTimeframeServiceTest {
assertFalse(service.usesTimeframe(1L, "1m"));
}
@Test
void collectForStrategy_reads3mAnd5mWithout1m() {
GcStrategy onlyUpper = new GcStrategy();
onlyUpper.setId(4L);
onlyUpper.setBuyConditionJson("""
{
"type": "TIMEFRAME",
"candleType": "3m",
"candleTypes": ["3m", "5m"],
"children": [{
"type": "CONDITION",
"condition": { "indicatorType": "RSI", "period": 14 }
}]
}
""");
when(strategyRepo.findById(4L)).thenReturn(Optional.of(onlyUpper));
Set<String> tfs = service.collectForStrategy(4L);
assertEquals(Set.of("3m", "5m"), tfs);
assertFalse(service.usesTimeframe(4L, "1m"));
assertTrue(service.usesTimeframe(4L, "3m"));
assertTrue(service.usesTimeframe(4L, "5m"));
}
@Test
void collectForStrategy_readsMultipleCandleTypesFromTimeframeNode() {
GcStrategy multi = new GcStrategy();
@@ -11,6 +11,44 @@ class StrategyDslTimeframeNormalizerTest {
private final ObjectMapper objectMapper = new ObjectMapper();
private final StrategyDslTimeframeNormalizer normalizer = new StrategyDslTimeframeNormalizer(objectMapper);
@Test
void normalize_preserves3mAnd5mWithout1m() throws Exception {
String json = """
{
"type": "TIMEFRAME",
"candleType": "1m",
"candleTypes": ["3m", "5m"],
"children": [{
"type": "CONDITION",
"condition": { "indicatorType": "RSI", "period": 14 }
}]
}
""";
String normalized = normalizer.normalizeJson(json, "테스트");
JsonNode root = objectMapper.readTree(normalized);
assertEquals(2, root.path("candleTypes").size());
assertEquals("3m", root.path("candleTypes").get(0).asText());
assertEquals("5m", root.path("candleTypes").get(1).asText());
assertEquals("3m", root.path("candleType").asText());
}
@Test
void normalize_preservesSingleExplicit3m() throws Exception {
String json = """
{
"type": "TIMEFRAME",
"candleType": "3m",
"children": [{
"type": "CONDITION",
"condition": { "indicatorType": "RSI", "period": 14 }
}]
}
""";
String normalized = normalizer.normalizeJson(json, "테스트");
JsonNode root = objectMapper.readTree(normalized);
assertEquals("3m", root.path("candleType").asText());
}
@Test
void normalize_preservesMultiCandleTypesOnSave() throws Exception {
String json = """