1분봉 오류 수정
This commit is contained in:
+20
-10
@@ -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) {
|
private Set<String> sanitizeEvaluationTimeframes(Set<String> scope, GcStrategy strategy) {
|
||||||
Set<String> explicit = collectExplicitFromStrategy(strategy);
|
if (!scope.contains("1m") || scope.size() <= 1) return scope;
|
||||||
if (explicit.isEmpty()) return scope;
|
|
||||||
if (!explicit.contains("1m") && scope.contains("1m")) {
|
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);
|
Set<String> filtered = new LinkedHashSet<>(scope);
|
||||||
filtered.remove("1m");
|
filtered.remove("1m");
|
||||||
return filtered.isEmpty() ? scope : filtered;
|
return filtered.isEmpty() ? scope : filtered;
|
||||||
@@ -116,19 +125,20 @@ public class StrategyConditionTimeframeService {
|
|||||||
return scope;
|
return scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> collectExplicitFromStrategy(GcStrategy strategy) {
|
private Set<String> collectConditionDeclaredFromStrategy(GcStrategy strategy) {
|
||||||
Set<String> out = new LinkedHashSet<>();
|
Set<String> out = new LinkedHashSet<>();
|
||||||
collectExplicitFromJson(strategy.getBuyConditionJson(), out);
|
collectConditionDeclaredFromJson(strategy.getBuyConditionJson(), out);
|
||||||
collectExplicitFromJson(strategy.getSellConditionJson(), out);
|
collectConditionDeclaredFromJson(strategy.getSellConditionJson(), out);
|
||||||
return 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;
|
if (json == null || json.isBlank()) return;
|
||||||
try {
|
try {
|
||||||
out.addAll(StrategyDslTimeframeNormalizer.collectExplicitCandleTypes(objectMapper.readTree(json)));
|
out.addAll(StrategyDslTimeframeNormalizer.collectConditionDeclaredCandleTypes(
|
||||||
|
objectMapper.readTree(json)));
|
||||||
} catch (Exception e) {
|
} 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;
|
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) {
|
private static void collectExplicitCandleTypes(JsonNode node, Set<String> out) {
|
||||||
if (node == null || node.isNull()) return;
|
if (node == null || node.isNull()) return;
|
||||||
|
|
||||||
@@ -314,14 +344,29 @@ public class StrategyDslTimeframeNormalizer {
|
|||||||
return fromConditions;
|
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) {
|
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;
|
return fromStart;
|
||||||
}
|
}
|
||||||
Set<String> filtered = new LinkedHashSet<>(fromStart);
|
if (fromConditions.size() > 1 && fromConditions.contains("1m")) {
|
||||||
filtered.remove("1m");
|
return fromStart;
|
||||||
return filtered.isEmpty() ? fromStart : filtered;
|
}
|
||||||
|
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) {
|
private ObjectNode writeResolvedTimeframe(JsonNode root, String strategyName) {
|
||||||
@@ -368,8 +413,8 @@ public class StrategyDslTimeframeNormalizer {
|
|||||||
if (!arr.isArray() || arr.size() < 2) return copy;
|
if (!arr.isArray() || arr.size() < 2) return copy;
|
||||||
|
|
||||||
JsonNode subtree = firstTimeframeChild(tf);
|
JsonNode subtree = firstTimeframeChild(tf);
|
||||||
Set<String> explicit = collectExplicitCandleTypes(subtree);
|
Set<String> conditionDeclared = collectConditionDeclaredCandleTypes(subtree);
|
||||||
if (explicit.contains("1m")) return copy;
|
if (conditionDeclared.contains("1m")) return copy;
|
||||||
|
|
||||||
List<String> normalized = new ArrayList<>();
|
List<String> normalized = new ArrayList<>();
|
||||||
for (JsonNode t : arr) {
|
for (JsonNode t : arr) {
|
||||||
|
|||||||
+41
@@ -276,6 +276,47 @@ class StrategyConditionTimeframeServiceTest {
|
|||||||
assertTrue(service.usesTimeframe(7L, "3m"));
|
assertTrue(service.usesTimeframe(7L, "3m"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void collectForStrategy_flowLayoutStale1mRemovedWhenConditionsAreUpperOnly() {
|
||||||
|
GcStrategy strategy = new GcStrategy();
|
||||||
|
strategy.setId(8L);
|
||||||
|
strategy.setName("CCI 3분 5분 10분 15분 1시간");
|
||||||
|
strategy.setFlowLayoutJson("""
|
||||||
|
{
|
||||||
|
"buy": {
|
||||||
|
"startMeta": {
|
||||||
|
"start-0": {
|
||||||
|
"candleTypes": ["1m", "3m", "5m", "10m", "15m", "1h"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
strategy.setBuyConditionJson("""
|
||||||
|
{
|
||||||
|
"type": "TIMEFRAME",
|
||||||
|
"candleType": "3m",
|
||||||
|
"candleTypes": ["3m", "5m", "10m", "15m", "1h"],
|
||||||
|
"children": [{
|
||||||
|
"type": "AND",
|
||||||
|
"children": [
|
||||||
|
{ "type": "CONDITION", "condition": { "indicatorType": "CCI", "leftCandleType": "3m" } },
|
||||||
|
{ "type": "CONDITION", "condition": { "indicatorType": "CCI", "leftCandleType": "5m" } }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
when(strategyRepo.findById(8L)).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 -> inv.getArgument(0));
|
||||||
|
|
||||||
|
Set<String> tfs = service.collectForStrategy(8L);
|
||||||
|
assertEquals(Set.of("3m", "5m", "10m", "15m", "1h"), tfs);
|
||||||
|
assertFalse(service.usesTimeframe(8L, "1m"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void collectForStrategy_legacyTreeDefaultsTo1m() {
|
void collectForStrategy_legacyTreeDefaultsTo1m() {
|
||||||
GcStrategy legacy = new GcStrategy();
|
GcStrategy legacy = new GcStrategy();
|
||||||
|
|||||||
@@ -186,6 +186,29 @@ class StrategyDslTimeframeNormalizerTest {
|
|||||||
assertEquals("15m", root.path("candleType").asText());
|
assertEquals("15m", root.path("candleType").asText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveStartCandleTypes_dropsStale1mWhenAllConditionsDefaultTo1m() throws Exception {
|
||||||
|
String json = """
|
||||||
|
{
|
||||||
|
"type": "TIMEFRAME",
|
||||||
|
"candleType": "1m",
|
||||||
|
"candleTypes": ["1m", "3m", "5m", "10m", "15m", "1h"],
|
||||||
|
"children": [{
|
||||||
|
"type": "AND",
|
||||||
|
"children": [
|
||||||
|
{ "type": "CONDITION", "condition": { "indicatorType": "CCI", "leftCandleType": "1m" } },
|
||||||
|
{ "type": "CONDITION", "condition": { "indicatorType": "CCI", "leftCandleType": "1m" } },
|
||||||
|
{ "type": "CONDITION", "condition": { "indicatorType": "CCI", "leftCandleType": "1m" } }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
JsonNode root = objectMapper.readTree(json);
|
||||||
|
assertEquals(
|
||||||
|
Set.of("3m", "5m", "10m", "15m", "1h"),
|
||||||
|
StrategyDslTimeframeNormalizer.resolveStartCandleTypes(root));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolveStartCandleTypes_dropsStale1mWhenConditionsUseMultipleUpperTfs() throws Exception {
|
void resolveStartCandleTypes_dropsStale1mWhenConditionsUseMultipleUpperTfs() throws Exception {
|
||||||
String json = """
|
String json = """
|
||||||
|
|||||||
Reference in New Issue
Block a user