일목균형표 수정

This commit is contained in:
Macbook
2026-05-27 23:36:48 +09:00
parent 9cee6387c3
commit 8cc0d1c88c
73 changed files with 2256 additions and 334 deletions
@@ -55,6 +55,28 @@ class StrategyConditionTimeframeServiceTest {
assertFalse(service.usesTimeframe(1L, "1m"));
}
@Test
void collectForStrategy_readsMultipleCandleTypesFromTimeframeNode() {
GcStrategy multi = new GcStrategy();
multi.setId(3L);
multi.setBuyConditionJson("""
{
"type": "TIMEFRAME",
"candleType": "1m",
"candleTypes": ["1m", "3m", "5m"],
"children": [{
"type": "CONDITION",
"condition": { "indicatorType": "RSI", "period": 14 }
}]
}
""");
when(strategyRepo.findById(3L)).thenReturn(Optional.of(multi));
Set<String> tfs = service.collectForStrategy(3L);
assertEquals(Set.of("1m", "3m", "5m"), tfs);
assertTrue(service.usesTimeframe(3L, "3m"));
}
@Test
void collectForStrategy_legacyTreeDefaultsTo1m() {
GcStrategy legacy = new GcStrategy();
@@ -0,0 +1,35 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class StrategyDslTimeframeNormalizerTest {
private final ObjectMapper objectMapper = new ObjectMapper();
private final StrategyDslTimeframeNormalizer normalizer = new StrategyDslTimeframeNormalizer(objectMapper);
@Test
void normalize_preservesMultiCandleTypesOnSave() throws Exception {
String json = """
{
"type": "TIMEFRAME",
"candleType": "1m",
"candleTypes": ["1m", "3m", "5m"],
"children": [{
"type": "CONDITION",
"condition": { "indicatorType": "RSI", "period": 14 }
}]
}
""";
String normalized = normalizer.normalizeJson(json, "5분봉 테스트 전략");
JsonNode root = objectMapper.readTree(normalized);
assertTrue(root.path("candleTypes").isArray());
assertEquals(3, root.path("candleTypes").size());
assertEquals("1m", root.path("candleTypes").get(0).asText());
assertEquals("3m", root.path("candleTypes").get(1).asText());
assertEquals("5m", root.path("candleTypes").get(2).asText());
}
}
@@ -0,0 +1,85 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goldenchart.entity.GcStrategy;
import com.goldenchart.repository.GcStrategyRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
/**
* START candleTypes → BarBuilder 게이트(usesTimeframe) → TriggerBranch 평가 연결 검증.
*/
@ExtendWith(MockitoExtension.class)
class StrategyEvaluationTimeframeIntegrationTest {
@Mock
private GcStrategyRepository strategyRepo;
@Spy
private ObjectMapper objectMapper = new ObjectMapper();
@Mock
private StrategyDslTimeframeNormalizer dslNormalizer;
@Mock
private LiveStrategyEvaluator liveStrategyEvaluator;
private StrategyConditionTimeframeService timeframeService;
private StrategyTriggerBranchEvaluator triggerEvaluator;
@BeforeEach
void setUp() {
timeframeService = new StrategyConditionTimeframeService(
strategyRepo, objectMapper, dslNormalizer, liveStrategyEvaluator);
triggerEvaluator = new StrategyTriggerBranchEvaluator(
new StrategyDslToTa4jAdapter(), objectMapper, new StrategyBranchStateCache());
}
@Test
void checkedTimeframes_gateBarCloseAndTriggerScope() throws Exception {
String buyJson = """
{
"type": "TIMEFRAME",
"candleType": "1m",
"candleTypes": ["1m", "3m", "5m"],
"children": [{
"type": "CONDITION",
"condition": {
"indicatorType": "RSI", "conditionType": "GT",
"leftField": "RSI_VALUE", "rightField": "K_0", "period": 14
}
}]
}
""";
GcStrategy strategy = new GcStrategy();
strategy.setId(100L);
strategy.setName("multi tf test");
strategy.setBuyConditionJson(buyJson);
when(strategyRepo.findById(100L)).thenReturn(Optional.of(strategy));
when(dslNormalizer.normalizeJson(buyJson, strategy.getName())).thenReturn(buyJson);
Set<String> collected = timeframeService.collectForStrategy(100L);
assertEquals(Set.of("1m", "3m", "5m"), collected);
assertTrue(timeframeService.usesTimeframe(100L, "1m"));
assertTrue(timeframeService.usesTimeframe(100L, "3m"));
assertTrue(timeframeService.usesTimeframe(100L, "5m"));
assertFalse(timeframeService.usesTimeframe(100L, "15m"));
var scope = triggerEvaluator.parseScope(buyJson);
assertEquals("OR", scope.combineOp());
assertEquals(3, scope.branches().size());
assertTrue(scope.branches().stream().anyMatch(b -> "3m".equals(b.candleType())));
}
}
@@ -110,6 +110,52 @@ class StrategyTriggerBranchEvaluatorTest {
assertTrue(rule1m.isSatisfied(idx1m, null));
}
@Test
void multiCandleTypes_sameSubtree_onlyEvaluatesTriggerBranch() throws Exception {
String json = """
{
"type": "TIMEFRAME",
"candleType": "1m",
"candleTypes": ["1m", "3m", "5m"],
"children": [{
"type": "CONDITION", "condition": {
"indicatorType": "RSI", "conditionType": "GT",
"leftField": "RSI_VALUE", "rightField": "K_0", "period": 14
}
}]
}
""";
var scope = evaluator.parseScope(json);
assertEquals("OR", scope.combineOp());
assertEquals(3, scope.branches().size());
assertEquals("1m", scope.branches().get(0).candleType());
assertEquals("3m", scope.branches().get(1).candleType());
assertEquals("5m", scope.branches().get(2).candleType());
Rule on1m = evaluator.buildTriggerRule(json, MARKET, STRATEGY_ID, "buy",
"1m", Map.of(), storage);
BarSeries s1m = storage.getOrCreate(MARKET, "1m");
on1m.isSatisfied(s1m.getEndIndex(), null);
assertNotNull(branchCache.get(MARKET, STRATEGY_ID, "buy", "1m"));
assertNull(branchCache.get(MARKET, STRATEGY_ID, "buy", "3m"),
"1m 마감 트리거 시 3m 분기는 평가·캐시되지 않아야 함");
assertNull(branchCache.get(MARKET, STRATEGY_ID, "buy", "5m"));
branchCache.invalidateStrategy(STRATEGY_ID);
Rule on3m = evaluator.buildTriggerRule(json, MARKET, STRATEGY_ID, "buy",
"3m", Map.of(), storage);
BarSeries s3m = storage.getOrCreate(MARKET, "3m");
on3m.isSatisfied(s3m.getEndIndex(), null);
assertNotNull(branchCache.get(MARKET, STRATEGY_ID, "buy", "3m"));
assertNull(branchCache.get(MARKET, STRATEGY_ID, "buy", "1m"));
Rule on5mWrongTrigger = evaluator.buildTriggerRule(json, MARKET, STRATEGY_ID, "buy",
"1m", Map.of(), storage);
assertFalse(on5mWrongTrigger.isSatisfied(s3m.getEndIndex(), null),
"3m 인덱스로 1m 트리거 Rule 평가 시 false");
}
@Test
void single3mScope_onlyTriggersOn3mBranch() throws Exception {
String json = """