Files
goldenChart/backend/src/test/java/com/goldenchart/service/RsiThresholdCrossSignalTest.java
T
Macbook bd6094f71f 백엔드 Docker 빌드 실패로 배포가 막히던 문제를 수정한다.
테스트 컴파일 오류(RsiThresholdCrossSignalTest)와 -DskipTests 한계를 해결해
StrategyConditionThresholdNormalizer가 실제 서버 이미지에 포함되도록 한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 00:30:10 +09:00

162 lines
5.8 KiB
Java

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 java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* RSI CROSS_UP — HL_MID(중앙선) vs K_50(직접입력) 동일 임계값 시그널 일치.
*/
class RsiThresholdCrossSignalTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
private StrategyDslToTa4jAdapter adapter;
private BarSeries series;
@BeforeEach
void setUp() {
adapter = new StrategyDslToTa4jAdapter();
series = buildOscillatingSeries(200);
}
@Test
void rsiCrossUp_hlMid_equalsK50Override() throws Exception {
assertSignalHitsEqual(
"""
{
"indicatorType": "RSI",
"conditionType": "CROSS_UP",
"leftField": "RSI_VALUE_9",
"rightField": "HL_MID",
"period": 9,
"valuePeriodOverride": true,
"thresholdOverride": false,
"candleRange": 1
}
""",
"""
{
"indicatorType": "RSI",
"conditionType": "CROSS_UP",
"leftField": "RSI_VALUE_9",
"rightField": "K_50",
"period": 9,
"valuePeriodOverride": true,
"thresholdOverride": true,
"targetValue": 50,
"candleRange": 1
}
""");
}
@Test
void rsiCrossUp_hlMid_equalsK50WithoutOverride() throws Exception {
assertSignalHitsEqual(
"""
{
"indicatorType": "RSI",
"conditionType": "CROSS_UP",
"leftField": "RSI_VALUE_9",
"rightField": "HL_MID",
"period": 9,
"valuePeriodOverride": true,
"candleRange": 1
}
""",
"""
{
"indicatorType": "RSI",
"conditionType": "CROSS_UP",
"leftField": "RSI_VALUE_9",
"rightField": "K_50",
"period": 9,
"valuePeriodOverride": true,
"candleRange": 1
}
""");
}
private void assertSignalHitsEqual(String condA, String condB) throws Exception {
Map<String, Map<String, Object>> indicatorParams = Map.of(
"RSI", Map.of("length", 9, "maLength", 3, "maType", "SMA", "src", "close"));
Map<String, Map<String, Object>> visual = Map.of(
"RSI", Map.of("hlines", List.of(
Map.of("label", "과열선", "price", 70),
Map.of("label", "중앙선", "price", 50),
Map.of("label", "침체선", "price", 30))));
List<Integer> hitsA = scanHits(condA, indicatorParams, visual);
List<Integer> hitsB = scanHits(condB, indicatorParams, visual);
assertEquals(hitsA, hitsB,
"HL_MID and K_50 should produce identical CROSS_UP bars, A=" + hitsA.size() + " B=" + hitsB.size());
}
private List<Integer> scanHits(String condJson,
Map<String, Map<String, Object>> indicatorParams,
Map<String, Map<String, Object>> visual) throws Exception {
JsonNode buyDsl = MAPPER.readTree("""
{
"type": "TIMEFRAME",
"candleTypes": ["3m"],
"candleType": "3m",
"children": [{
"type": "CONDITION",
"condition": %s
}]
}
""".formatted(condJson.trim()));
StrategyDslToTa4jAdapter.RuleBuildContext ctx =
new StrategyDslToTa4jAdapter.RuleBuildContext(
series, indicatorParams, visual, null, null, false, Map.of());
Rule rule = adapter.toRule(buyDsl, ctx);
java.util.ArrayList<Integer> hits = new java.util.ArrayList<>();
for (int i = series.getBeginIndex(); i <= series.getEndIndex(); i++) {
if (rule.isSatisfied(i, null)) hits.add(i);
}
return hits;
}
private static BarSeries buildOscillatingSeries(int count) {
Duration period = Duration.ofMinutes(3);
BarSeries s = new BaseBarSeriesBuilder()
.withName("test")
.withBarBuilderFactory(new TimeBarBuilderFactory())
.withNumFactory(DoubleNumFactory.getInstance())
.build();
TimeBarBuilderFactory factory = new TimeBarBuilderFactory();
Instant t = Instant.parse("2024-01-01T00:00:00Z");
double price = 100;
for (int i = 0; i < count; i++) {
double swing = Math.sin(i * 0.15) * 8 + Math.sin(i * 0.04) * 4;
price = 100 + swing + (i * 0.02);
double o = price - 0.5;
double h = price + 1.5;
double l = price - 1.5;
double c = price;
factory.createBarBuilder(s)
.timePeriod(period)
.endTime(t.plus(period))
.openPrice(o).highPrice(h).lowPrice(l).closePrice(c)
.volume(1000)
.add();
t = t.plus(period);
}
return s;
}
}