N봉 기울기 판단로직 수정

This commit is contained in:
Macbook
2026-06-16 09:12:24 +09:00
parent 518b69cdc6
commit f85c0bef20
10 changed files with 642 additions and 367 deletions
@@ -0,0 +1,149 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseBarSeriesBuilder;
import org.ta4j.core.Rule;
import org.ta4j.core.bars.TimeBarBuilderFactory;
import org.ta4j.core.num.DoubleNumFactory;
import java.time.Duration;
import java.time.Instant;
import static org.junit.jupiter.api.Assertions.*;
/**
* 기울기 + N봉 내 존재 — N봉 구간 전체 추세 판정 (임의 시점 짧은 기울기 존재 여부 아님).
*/
class SlopeCandleRangeRuleTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
private StrategyDslToTa4jAdapter adapter;
private BarSeries series;
@BeforeEach
void setUp() {
adapter = new StrategyDslToTa4jAdapter();
// 중간 3봉만 상승, 전체 10봉은 횡보·하락 혼재 → 짧은 기울기만 존재
series = buildOscillatingSeries(new double[] {
100, 101, 100, 101, 100, 101, 102, 103, 102, 101
});
}
@Test
void slopeUp_existsIn_usesFullWindowTrend_notAnyShortRiseInWindow() throws Exception {
Rule rule = toSlopeRule("""
"candleRangeMode": "EXISTS_IN",
"candleRange": 10,
"slopePeriod": 3
""", "SLOPE_UP");
int idx = series.getEndIndex();
assertFalse(rule.isSatisfied(idx, null),
"10봉 구간 전체가 상승 추세가 아니면 false (중간 3봉 상승만으로는 true 아님)");
}
@Test
void slopeUp_current_usesSlopePeriod() throws Exception {
BarSeries rising = buildOscillatingSeries(new double[] {
100, 101, 102, 103, 104
});
Rule rule = toSlopeRule("""
"candleRangeMode": "CURRENT",
"slopePeriod": 3
""", "SLOPE_UP", rising);
assertTrue(rule.isSatisfied(rising.getEndIndex(), null),
"현재 봉 기준 slopePeriod 구간 상승이면 true");
}
@Test
void slopeUp_existsIn_monotonicWindow_true() throws Exception {
BarSeries rising = buildOscillatingSeries(new double[] {
100, 101, 102, 103, 104, 105, 106, 107, 108, 109
});
Rule rule = toSlopeRule("""
"candleRangeMode": "EXISTS_IN",
"candleRange": 10,
"slopePeriod": 3
""", "SLOPE_UP", rising);
assertTrue(rule.isSatisfied(rising.getEndIndex(), null),
"10봉 구간 전체 상승 추세이면 true");
}
@Test
void slopeDown_notExistsIn_trueWhenWindowNotFalling() throws Exception {
Rule rule = toSlopeRule("""
"candleRangeMode": "NOT_EXISTS_IN",
"candleRange": 10,
"slopePeriod": 3
""", "SLOPE_DOWN");
assertTrue(rule.isSatisfied(series.getEndIndex(), null),
"10봉 구간이 하락 추세가 아니면 NOT_EXISTS_IN(SLOPE_DOWN) true");
}
@Test
void candleRangePrefix_slopeExistsIn_usesPeriodWording() throws Exception {
JsonNode cond = MAPPER.readTree("""
{
"conditionType": "SLOPE_UP",
"candleRangeMode": "EXISTS_IN",
"candleRange": 8
}
""");
assertEquals("최근 8봉 구간 ", StrategyDslToTa4jAdapter.candleRangeConditionPrefix(cond));
}
private Rule toSlopeRule(String extraFields, String condType) throws Exception {
return toSlopeRule(extraFields, condType, series);
}
private Rule toSlopeRule(String extraFields, String condType, BarSeries s) throws Exception {
String extra = extraFields == null || extraFields.isBlank()
? ""
: ",\n " + extraFields.strip().replaceAll(",\\s*$", "");
String json = """
{
"id": "c-slope",
"type": "CONDITION",
"condition": {
"indicatorType": "MA",
"conditionType": "%s",
"leftField": "CLOSE_PRICE",
"rightField": "NONE"%s
}
}
""".formatted(condType, extra);
JsonNode node = MAPPER.readTree(json);
Rule rule = adapter.toRule(node, s, java.util.Map.of());
assertNotNull(rule);
return rule;
}
private static BarSeries buildOscillatingSeries(double[] closes) {
BarSeries s = new BaseBarSeriesBuilder()
.withName("test-slope")
.withNumFactory(DoubleNumFactory.getInstance())
.withBarBuilderFactory(new TimeBarBuilderFactory())
.build();
TimeBarBuilderFactory factory = new TimeBarBuilderFactory();
Instant base = Instant.parse("2024-06-01T00:00:00Z");
Duration period = Duration.ofMinutes(1);
for (int i = 0; i < closes.length; i++) {
double close = closes[i];
Instant end = base.plus(period.multipliedBy(i + 1L));
factory.createBarBuilder(s)
.timePeriod(period)
.endTime(end)
.openPrice(close - 1)
.highPrice(close + 1)
.lowPrice(close - 1)
.closePrice(close)
.volume(10 + i)
.add();
}
return s;
}
}