전략평가 로직 오류 수정

This commit is contained in:
Macbook
2026-06-17 01:00:16 +09:00
parent b168498421
commit f1ce499809
7 changed files with 463 additions and 66 deletions
@@ -0,0 +1,116 @@
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.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* CCI 본선(CCI_VALUE_20) ↔ 신호선(CCI_SIGNAL) 교차 — 차트와 동일 Rule 평가.
*/
class CciCrossSignalScanTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
private StrategyDslToTa4jAdapter adapter;
private BarSeries series;
@BeforeEach
void setUp() {
adapter = new StrategyDslToTa4jAdapter();
series = buildOscillatingSeries(150);
}
@Test
void cciSignal_usesLeftFieldPeriod_notGlobalLength() throws Exception {
JsonNode cond = MAPPER.readTree("""
{
"indicatorType": "CCI",
"conditionType": "CROSS_UP",
"leftField": "CCI_VALUE_20",
"rightField": "CCI_SIGNAL",
"period": 20,
"valuePeriodOverride": true
}
""");
Map<String, Object> params = Map.of("length", 13, "maLength", 10, "maType", "SMA", "src", "hlc3");
var value20 = adapter.resolveIndicatorFieldForTest(
"CCI_VALUE_20", "CCI", params, series, 20, -1, cond);
var signalLinked = adapter.resolveIndicatorFieldForTest(
"CCI_SIGNAL", "CCI", params, series, 20, -1, cond);
var signalUnlinked = adapter.resolveIndicatorFieldForTest(
"CCI_SIGNAL", "CCI", params, series, -1, -1, null);
int idx = 80;
assertNotEquals(signalLinked.getValue(idx).doubleValue(), signalUnlinked.getValue(idx).doubleValue(), 1e-9,
"CCI_SIGNAL must use CCI(20) from leftField, not global length=13");
assertNotEquals(value20.getValue(idx).doubleValue(), signalLinked.getValue(idx).doubleValue(), 1e-9,
"signal line is SMA(CCI), not raw CCI");
}
@Test
void cciCrossUp_firesOnAtLeastOneBar() throws Exception {
JsonNode buyDsl = MAPPER.readTree("""
{
"type": "TIMEFRAME",
"candleType": "5m",
"children": [{
"type": "CONDITION",
"condition": {
"indicatorType": "CCI",
"conditionType": "CROSS_UP",
"leftField": "CCI_VALUE_20",
"rightField": "CCI_SIGNAL",
"period": 20,
"valuePeriodOverride": true,
"candleRange": 1
}
}]
}
""");
Map<String, Map<String, Object>> indicatorParams = Map.of(
"CCI", Map.of("length", 20, "maLength", 10, "maType", "SMA", "src", "hlc3"));
StrategyDslToTa4jAdapter.RuleBuildContext ctx =
new StrategyDslToTa4jAdapter.RuleBuildContext(
series, indicatorParams, Map.of(), null, null, false, Map.of());
Rule rule = adapter.toRule(buyDsl, ctx);
int hits = 0;
for (int i = series.getBeginIndex(); i <= series.getEndIndex(); i++) {
if (rule.isSatisfied(i, null)) hits++;
}
assertTrue(hits > 0, "CCI(20) cross signal(10) should fire at least once on oscillating series, got " + hits);
}
private static BarSeries buildOscillatingSeries(int count) {
BaseBarSeriesBuilder builder = new BaseBarSeriesBuilder()
.withName("cci-cross-test")
.withNumFactory(DoubleNumFactory.getInstance())
.withBarBuilderFactory(new TimeBarBuilderFactory(Duration.ofMinutes(5), 1));
BarSeries s = builder.build();
Instant t = Instant.parse("2024-06-01T00:00:00Z");
double price = 100_000_000.0;
for (int i = 0; i < count; i++) {
double wave = Math.sin(i * 0.22) * 800_000 + Math.sin(i * 0.07) * 400_000;
double close = price + wave + i * 5000;
double open = close - 20_000;
double high = close + 80_000;
double low = close - 80_000;
s.addBar(t, open, high, low, close, 50 + i);
t = t.plus(Duration.ofMinutes(5));
}
return s;
}
}
@@ -0,0 +1,113 @@
package com.goldenchart.service;
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.Indicator;
import org.ta4j.core.bars.TimeBarBuilderFactory;
import org.ta4j.core.num.DoubleNumFactory;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* 조건 노드 period / leftPeriod / rightPeriod 가 각 지표 필드에 반영되는지 검증.
* (CCI·RSI·TRIX 등 본선/신호선 교차 시 동일 기간 사용)
*/
class OscillatorPeriodOverrideTest {
private StrategyDslToTa4jAdapter adapter;
private BarSeries series;
@BeforeEach
void setUp() {
adapter = new StrategyDslToTa4jAdapter();
series = buildSeries(120);
}
@Test
void trixSignal_respectsConditionPeriod() {
Map<String, Object> p = Map.of("length", 12, "signalLength", 9);
Indicator<?> defaultSig = adapter.resolveIndicatorFieldForTest(
"TRIX_SIGNAL", "TRIX", p, series, -1, -1);
Indicator<?> overrideSig = adapter.resolveIndicatorFieldForTest(
"TRIX_SIGNAL", "TRIX", p, series, 20, -1);
assertValuesDiffer(defaultSig, overrideSig, 80);
}
@Test
void stochK_respectsConditionPeriod() {
Map<String, Object> p = Map.of("kLength", 14, "smooth", 3, "dSmoothing", 3);
Indicator<?> defaultK = adapter.resolveIndicatorFieldForTest(
"STOCH_K", "STOCHASTIC", p, series, -1, -1);
Indicator<?> overrideK = adapter.resolveIndicatorFieldForTest(
"STOCH_K", "STOCHASTIC", p, series, 21, -1);
assertValuesDiffer(defaultK, overrideK, 90);
}
@Test
void williamsR_respectsConditionPeriod() {
Map<String, Object> p = Map.of("length", 14);
Indicator<?> defaultW = adapter.resolveIndicatorFieldForTest(
"WILLIAMS_R_VALUE", "WILLIAMS_R", p, series, -1, -1);
Indicator<?> overrideW = adapter.resolveIndicatorFieldForTest(
"WILLIAMS_R_VALUE", "WILLIAMS_R", p, series, 20, -1);
assertValuesDiffer(defaultW, overrideW, 90);
}
@Test
void bwiValue_resolvesInsteadOfCloseFallback() {
Map<String, Object> p = Map.of("length", 20, "mult", 2.0);
Indicator<?> bwi = adapter.resolveIndicatorFieldForTest(
"BWI_VALUE", "BWI", p, series, -1, -1);
Indicator<?> close = adapter.resolveIndicatorFieldForTest(
"UNKNOWN_FIELD", "BWI", p, series, -1, -1);
assertValuesDiffer(bwi, close, 80);
}
@Test
void cciSignalAndValue_useSamePeriodWhenCondPeriodSet() {
Map<String, Object> p = Map.of("length", 13, "maLength", 10, "maType", "SMA");
Indicator<?> value = adapter.resolveIndicatorFieldForTest(
"CCI_VALUE", "CCI", p, series, 20, -1);
Indicator<?> signal = adapter.resolveIndicatorFieldForTest(
"CCI_SIGNAL", "CCI", p, series, 20, -1);
// 동일 condPeriod → 신호선은 본선 SMA이므로 값이 항상 같지는 않지만,
// 기본(13) 대비 override(20) 시 둘 다 변해야 함
Indicator<?> valueDefault = adapter.resolveIndicatorFieldForTest(
"CCI_VALUE", "CCI", p, series, -1, -1);
assertNotEquals(value.getValue(80).doubleValue(), valueDefault.getValue(80).doubleValue(), 1e-6);
assertNotEquals(signal.getValue(80).doubleValue(),
adapter.resolveIndicatorFieldForTest("CCI_SIGNAL", "CCI", p, series, -1, -1)
.getValue(80).doubleValue(), 1e-6);
}
private static void assertValuesDiffer(Indicator<?> a, Indicator<?> b, int index) {
assertNotEquals(a.getValue(index).doubleValue(), b.getValue(index).doubleValue(), 1e-9,
"period override should change indicator values at index " + index);
}
private static BarSeries buildSeries(int count) {
BaseBarSeriesBuilder builder = new BaseBarSeriesBuilder()
.withName("test")
.withNumFactory(DoubleNumFactory.getInstance())
.withBarBuilderFactory(new TimeBarBuilderFactory(Duration.ofMinutes(1), 1));
BarSeries s = builder.build();
Instant t = Instant.parse("2024-01-01T00:00:00Z");
double price = 100.0;
for (int i = 0; i < count; i++) {
double drift = Math.sin(i * 0.15) * 3 + i * 0.05;
double close = price + drift;
double open = close - 0.3;
double high = close + 1.2;
double low = close - 1.0;
s.addBar(t, open, high, low, close, 1000 + i * 10);
t = t.plus(Duration.ofMinutes(1));
}
return s;
}
}