전략평가 로직 오류 수정

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
@@ -463,9 +463,9 @@ public class StrategyDslToTa4jAdapter {
buildEqRule(left, right, series));
case "EQ" -> buildEqRule(left, right, series);
case "NEQ" -> new NotRule(buildEqRule(left, right, series));
// CrossedUp/Down: ta4j 0.22 에서는 2인자 생성자만 있음
case "CROSS_UP" -> new CrossedUpIndicatorRule(left, right);
case "CROSS_DOWN" -> new CrossedDownIndicatorRule(left, right);
// 직전봉≤·현재봉> 교차 (차트·CrossTimeframeCompositeRule 과 동일, NaN 구간 제외)
case "CROSS_UP" -> buildCrossUpRule(left, right);
case "CROSS_DOWN" -> buildCrossDownRule(left, right);
case "SLOPE_UP" -> buildSlopeTrendRule(left, true, candleRangeMode, candleRange, slopePeriod);
case "SLOPE_DOWN" -> buildSlopeTrendRule(left, false, candleRangeMode, candleRange, slopePeriod);
case "DIFF_GT" -> {
@@ -750,7 +750,7 @@ public class StrategyDslToTa4jAdapter {
yield new ConstantIndicator<>(s, s.numFactory().numOf(constant));
}
Map<String, Object> sma = ctx != null ? ctx.smaParams() : Map.of();
yield resolveIndicatorField(field, indType, p, sma, s, condPeriod, sidePeriod);
yield resolveIndicatorField(field, indType, p, sma, s, condPeriod, sidePeriod, cond);
}
};
}
@@ -787,7 +787,8 @@ public class StrategyDslToTa4jAdapter {
Map<String, Object> p,
Map<String, Object> smaParams,
BarSeries s,
int condPeriod, int sidePeriod) {
int condPeriod, int sidePeriod,
JsonNode cond) {
ClosePriceIndicator close = new ClosePriceIndicator(s);
int periodOverride = sidePeriod > 0 ? sidePeriod : (condPeriod > 0 ? condPeriod : -1);
@@ -798,13 +799,13 @@ public class StrategyDslToTa4jAdapter {
}
if (field.equals("CCI_VALUE") || indType.equals("CCI"))
return new CciOnSourceIndicator(resolvePriceSource(s, CciOnSourceIndicator.normalizeParams(p)),
periodOverride > 0 ? periodOverride : intP(p, "length", 13));
effectivePeriod(periodOverride, p, "length", 13));
if (field.equals("CCI_SIGNAL")) {
Map<String, Object> norm = CciOnSourceIndicator.normalizeParams(p);
int cciLen = periodOverride > 0 ? periodOverride : intP(p, "length", 13);
int cciLen = resolveLinkedBasePeriod(cond, p, "CCI_VALUE", periodOverride, "length", 13);
CciOnSourceIndicator cci = new CciOnSourceIndicator(resolvePriceSource(s, norm), cciLen);
String maType = p != null ? p.getOrDefault("maType", "SMA").toString() : "SMA";
int maLen = intP(p, "maLength", 10);
int maLen = resolveSignalSmoothingPeriod(cond, sidePeriod, p, "maLength", 10);
if ("None".equals(maType)) return cci;
return switch (maType) {
case "EMA" -> new EMAIndicator(cci, maLen);
@@ -818,12 +819,12 @@ public class StrategyDslToTa4jAdapter {
return new RSIIndicator(close, period);
}
if (field.equals("RSI_VALUE"))
return new RSIIndicator(close, periodOverride > 0 ? periodOverride : intP(p, "length", 14));
return new RSIIndicator(close, effectivePeriod(periodOverride, p, "length", 14));
if (field.equals("RSI_SIGNAL")) {
int rsiLen = periodOverride > 0 ? periodOverride : intP(p, "length", 14);
int rsiLen = resolveLinkedBasePeriod(cond, p, "RSI_VALUE", periodOverride, "length", 14);
RSIIndicator rsi = new RSIIndicator(close, rsiLen);
String maType = p != null ? p.getOrDefault("maType", "SMA").toString() : "SMA";
int maLen = intP(p, "maLength", 14);
int maLen = resolveSignalSmoothingPeriod(cond, sidePeriod, p, "maLength", 14);
if ("None".equals(maType)) return rsi;
return switch (maType) {
case "EMA" -> new EMAIndicator(rsi, maLen);
@@ -831,17 +832,16 @@ public class StrategyDslToTa4jAdapter {
default -> new SMAIndicator(rsi, maLen);
};
}
// MACD
// MACD — cond.period → fastLength (프론트 대표 기간)
if (field.equals("MACD_LINE")) {
return new MACDIndicator(close, intP(p, "fastLength", 12), intP(p, "slowLength", 26));
return buildMacd(close, p, periodOverride);
}
if (field.equals("SIGNAL_LINE")) {
return new EMAIndicator(
new MACDIndicator(close, intP(p, "fastLength", 12), intP(p, "slowLength", 26)),
intP(p, "signalLength", 9));
MACDIndicator macd = buildMacd(close, p, periodOverride);
return new EMAIndicator(macd, intP(p, "signalLength", 9));
}
if (field.equals("HISTOGRAM")) {
MACDIndicator macd = new MACDIndicator(close, intP(p, "fastLength", 12), intP(p, "slowLength", 26));
MACDIndicator macd = buildMacd(close, p, periodOverride);
return NumericIndicator.of(macd).minus(new EMAIndicator(macd, intP(p, "signalLength", 9)));
}
// ADX / DMI
@@ -850,13 +850,14 @@ public class StrategyDslToTa4jAdapter {
return new ADXIndicator(s, intP(p, "diLength", 14), period);
}
if (field.equals("ADX_VALUE"))
return new ADXIndicator(s, intP(p, "diLength", 14), intP(p, "adxSmoothing", 14));
return new ADXIndicator(s, intP(p, "diLength", 14),
effectivePeriod(periodOverride, p, "adxSmoothing", 14));
if (field.equals("PDI") || field.equals("PLUS_DI")) {
int diLen = intP(p, "diLength", intP(p, "length", 14));
int diLen = effectivePeriod(periodOverride, p, "diLength", intP(p, "length", 14));
return new PlusDIIndicator(s, diLen);
}
if (field.equals("MDI") || field.equals("MINUS_DI")) {
int diLen = intP(p, "diLength", intP(p, "length", 14));
int diLen = effectivePeriod(periodOverride, p, "diLength", intP(p, "length", 14));
return new MinusDIIndicator(s, diLen);
}
// Stochastic Slow — IndicatorService.calcStochastic 과 동일한 로직:
@@ -864,12 +865,12 @@ public class StrategyDslToTa4jAdapter {
// slow %K = SMA(raw %K, smooth)
// %D = SMA(slow %K, dSmoothing)
if (field.equals("STOCH_K")) {
int kLen = intP(p, "kLength", 14);
int kLen = effectivePeriod(periodOverride, p, "kLength", 14);
int smooth = intP(p, "smooth", 3);
return new SMAIndicator(new StochasticOscillatorKIndicator(s, kLen), smooth);
}
if (field.equals("STOCH_D")) {
int kLen = intP(p, "kLength", 14);
int kLen = effectivePeriod(periodOverride, p, "kLength", 14);
int smooth = intP(p, "smooth", 3);
int dSmooth = intP(p, "dSmoothing", 3);
SMAIndicator slowK = new SMAIndicator(new StochasticOscillatorKIndicator(s, kLen), smooth);
@@ -878,29 +879,23 @@ public class StrategyDslToTa4jAdapter {
// TRIX — 종가 3×EMA ROC% (업비트·키움)
if (field.startsWith("TRIX_VALUE_")) {
int len = parseTrailingInt(field, "TRIX_VALUE_", intP(p, "length", 12));
EMAIndicator e3 = tripleEma(close, len);
PreviousValueIndicator prev3 = new PreviousValueIndicator(e3);
return NumericIndicator.of(e3).minus(prev3).dividedBy(prev3).multipliedBy(100);
return buildTrixLine(close, len);
}
if (field.equals("TRIX_VALUE")) {
int len = intP(p, "length", 12);
EMAIndicator e3 = tripleEma(close, len);
PreviousValueIndicator prev3 = new PreviousValueIndicator(e3);
return NumericIndicator.of(e3).minus(prev3).dividedBy(prev3).multipliedBy(100);
if (field.equals("TRIX_VALUE") || indType.equals("TRIX")) {
return buildTrixLine(close, effectivePeriod(periodOverride, p, "length", 12));
}
if (field.equals("TRIX_SIGNAL")) {
int len = intP(p, "length", 12);
EMAIndicator e3 = tripleEma(close, len);
PreviousValueIndicator prev3 = new PreviousValueIndicator(e3);
NumericIndicator trix = NumericIndicator.of(e3).minus(prev3).dividedBy(prev3).multipliedBy(100);
return new EMAIndicator(trix, intP(p, "signalLength", 9));
int trixLen = resolveLinkedBasePeriod(cond, p, "TRIX_VALUE", periodOverride, "length", 12);
NumericIndicator trix = buildTrixLine(close, trixLen);
int sigLen = resolveSignalSmoothingPeriod(cond, sidePeriod, p, "signalLength", 9);
return new EMAIndicator(trix, sigLen);
}
// OBV
if (field.equals("OBV_LINE")) return new OnBalanceVolumeIndicator(s);
if (field.equals("OBV_SIGNAL")) {
OnBalanceVolumeIndicator obv = new OnBalanceVolumeIndicator(s);
String maType = p != null ? p.getOrDefault("maType", "SMA").toString() : "SMA";
int maLen = intP(p, "maLength", 9);
int maLen = resolveSignalSmoothingPeriod(cond, sidePeriod, p, "maLength", 9);
return switch (maType) {
case "EMA" -> new EMAIndicator(obv, maLen);
case "WMA" -> new WMAIndicator(obv, maLen);
@@ -913,7 +908,7 @@ public class StrategyDslToTa4jAdapter {
return vrIndicator(s, period);
}
if (field.equals("VR_VALUE") || indType.equals("VR"))
return vrIndicator(s, intP(p, "length", 10));
return vrIndicator(s, effectivePeriod(periodOverride, p, "length", 10));
// 이격도 DISPARITY5 → period 5
if (field.startsWith("DISPARITY")) {
int period = parseTrailingInt(field, "DISPARITY", 20);
@@ -924,25 +919,33 @@ public class StrategyDslToTa4jAdapter {
return psychIndicator(s, period, false);
}
if (field.equals("PSY_VALUE") || indType.equals("PSYCHOLOGICAL"))
return psychIndicator(s, intP(p, "length", 12), false);
return psychIndicator(s, effectivePeriod(periodOverride, p, "length", 12), false);
if (field.startsWith("NEW_PSY_VALUE_")) {
int period = parseTrailingInt(field, "NEW_PSY_VALUE_", intP(p, "length", 10));
return newSentPsyIndicator(s, period);
}
if (field.equals("NEW_PSY_VALUE") || indType.equals("NEW_PSYCHOLOGICAL"))
return newSentPsyIndicator(s, intP(p, "length", 10));
return newSentPsyIndicator(s, effectivePeriod(periodOverride, p, "length", 10));
if (field.startsWith("INVEST_PSY_VALUE_")) {
int period = parseTrailingInt(field, "INVEST_PSY_VALUE_", intP(p, "length", 10));
return psychIndicator(s, period, false);
}
if (field.equals("INVEST_PSY_VALUE") || indType.equals("INVEST_PSYCHOLOGICAL"))
return psychIndicator(s, intP(p, "length", 10), false);
return psychIndicator(s, effectivePeriod(periodOverride, p, "length", 10), false);
// BWI (Bollinger Band Width)
if (field.startsWith("BWI_VALUE_")) {
int period = parseTrailingInt(field, "BWI_VALUE_", intP(p, "length", 20));
return bollingerBandWidth(s, p, period);
}
if (field.equals("BWI_VALUE") || indType.equals("BWI"))
return bollingerBandWidth(s, p, effectivePeriod(periodOverride, p, "length", 20));
// Williams %R
if (field.startsWith("WILLIAMS_R_VALUE_")) {
int period = parseTrailingInt(field, "WILLIAMS_R_VALUE_", intP(p, "length", 14));
return new WilliamsRIndicator(s, period);
}
if (field.equals("WILLIAMS_R_VALUE")) return new WilliamsRIndicator(s, intP(p, "length", 14));
if (field.equals("WILLIAMS_R_VALUE") || indType.equals("WILLIAMS_R"))
return new WilliamsRIndicator(s, effectivePeriod(periodOverride, p, "length", 14));
// 신고가·신저가 — 직전 N봉 최고/최저 (당일 봉 제외, PreviousValueIndicator)
if (field.startsWith("NH_PRIOR_")) {
int period = parseTrailingInt(field, "NH_PRIOR_", intP(p, "length", 9));
@@ -967,7 +970,7 @@ public class StrategyDslToTa4jAdapter {
}
// Bollinger Bands
if (field.equals("UPPER_BAND") || field.equals("LOWER_BAND") || field.equals("MIDDLE_BAND")) {
int len = intP(p, "length", 20);
int len = effectivePeriod(periodOverride, p, "length", 20);
double mult = dblP(p, "mult", 2.0);
String maType = p.getOrDefault("maType", "SMA").toString();
Indicator<Num> basis = maOfSource(close, s, maType, len);
@@ -1004,7 +1007,13 @@ public class StrategyDslToTa4jAdapter {
if (field.equals("LEADING_SPAN2")) return ichimokuSpanB(s, p);
if (field.equals("LAGGING_SPAN")) return ichimokuLagging(s, p);
// 거래량 MA
if (field.equals("VOLUME_MA")) return new SMAIndicator(new VolumeIndicator(s, 1), intP(p, "length", 20));
if (field.equals("VOLUME_VALUE") || indType.equals("VOLUME"))
return new VolumeIndicator(s, 1);
if (field.equals("VOLUME_MA"))
return new SMAIndicator(new VolumeIndicator(s, 1), effectivePeriod(periodOverride, p, "length", 20));
// 거래량 오실레이터 — cond.period → shortLength (프론트 대표 기간)
if (field.equals("VOLUME_OSC_VALUE") || indType.equals("VOLUME_OSC"))
return volumeOscillator(s, p, periodOverride);
log.warn("[Adapter] 미지원 필드: {} (indicatorType={})", field, indType);
return new ClosePriceIndicator(s);
@@ -1240,6 +1249,41 @@ public class StrategyDslToTa4jAdapter {
return new EMAIndicator(new EMAIndicator(new EMAIndicator(close, len), len), len);
}
/** TRIX 본선 — 종가 3×EMA ROC% */
private NumericIndicator buildTrixLine(ClosePriceIndicator close, int len) {
EMAIndicator e3 = tripleEma(close, len);
PreviousValueIndicator prev3 = new PreviousValueIndicator(e3);
return NumericIndicator.of(e3).minus(prev3).dividedBy(prev3).multipliedBy(100);
}
private MACDIndicator buildMacd(ClosePriceIndicator close, Map<String, Object> p, int periodOverride) {
int fast = effectivePeriod(periodOverride, p, "fastLength", 12);
return new MACDIndicator(close, fast, intP(p, "slowLength", 26));
}
/** BWI — IndicatorService.calcBBBandWidth 와 동일 */
private BollingerBandWidthIndicator bollingerBandWidth(BarSeries s, Map<String, Object> p, int len) {
ClosePriceIndicator close = new ClosePriceIndicator(s);
double mult = dblP(p, "mult", 2.0);
SMAIndicator sma = new SMAIndicator(close, len);
StandardDeviationIndicator std = StandardDeviationIndicator.ofSample(close, len);
BollingerBandsMiddleIndicator mid = new BollingerBandsMiddleIndicator(sma);
Num k = s.numFactory().numOf(mult);
BollingerBandsUpperIndicator upper = new BollingerBandsUpperIndicator(mid, std, k);
BollingerBandsLowerIndicator lower = new BollingerBandsLowerIndicator(mid, std, k);
return new BollingerBandWidthIndicator(upper, mid, lower);
}
/** VolumeOscillator — IndicatorService.calcVolumeOscillator 와 동일 */
private NumericIndicator volumeOscillator(BarSeries s, Map<String, Object> p, int periodOverride) {
int shortLen = effectivePeriod(periodOverride, p, "shortLength", 5);
int longLen = intP(p, "longLength", 10);
VolumeIndicator vol = new VolumeIndicator(s, 1);
EMAIndicator shortEma = new EMAIndicator(vol, shortLen);
EMAIndicator longEma = new EMAIndicator(vol, longLen);
return NumericIndicator.of(shortEma).minus(longEma).dividedBy(longEma).multipliedBy(100);
}
private Indicator<Num> maOfSource(Indicator<Num> source, BarSeries s, String maType, int len) {
return switch (maType) {
case "EMA" -> new EMAIndicator(source, len);
@@ -1463,6 +1507,76 @@ public class StrategyDslToTa4jAdapter {
// ── 파라미터 헬퍼 ─────────────────────────────────────────────────────────
/** 직전봉 left≤right → 현재봉 left>right (차트 교차·CrossTimeframeCompositeRule 과 동일) */
private Rule buildCrossUpRule(Indicator<Num> left, Indicator<Num> right) {
return new Rule() {
@Override
public boolean isSatisfied(int index, TradingRecord tradingRecord) {
if (index < 1) return false;
Num l0 = left.getValue(index - 1);
Num l1 = left.getValue(index);
Num r0 = right.getValue(index - 1);
Num r1 = right.getValue(index);
if (l0 == null || l1 == null || r0 == null || r1 == null) return false;
if (l0.isNaN() || l1.isNaN() || r0.isNaN() || r1.isNaN()) return false;
return l0.isLessThanOrEqual(r0) && l1.isGreaterThan(r1);
}
};
}
private Rule buildCrossDownRule(Indicator<Num> left, Indicator<Num> right) {
return new Rule() {
@Override
public boolean isSatisfied(int index, TradingRecord tradingRecord) {
if (index < 1) return false;
Num l0 = left.getValue(index - 1);
Num l1 = left.getValue(index);
Num r0 = right.getValue(index - 1);
Num r1 = right.getValue(index);
if (l0 == null || l1 == null || r0 == null || r1 == null) return false;
if (l0.isNaN() || l1.isNaN() || r0.isNaN() || r1.isNaN()) return false;
return l0.isGreaterThanOrEqual(r0) && l1.isLessThan(r1);
}
};
}
/**
* CCI_VALUE_20 등 좌측 본선 기간과 신호선(CCI_SIGNAL) 본선 기간을 일치시킨다.
* sidePeriod(우측)는 신호 SMA 길이용 — 본선 CCI/RSI/TRIX 기간으로 쓰지 않는다.
*/
private int resolveLinkedBasePeriod(JsonNode cond, Map<String, Object> p, String valuePrefix,
int periodOverride, String lengthKey, int defaultLen) {
if (cond != null && !cond.isNull()) {
String leftField = cond.path("leftField").asText("");
if (leftField.startsWith(valuePrefix + "_")) {
int fromField = parseTrailingInt(leftField, valuePrefix + "_", defaultLen);
if (fromField > 0) return fromField;
}
int leftPeriod = cond.path("leftPeriod").asInt(-1);
if (leftPeriod > 0) return leftPeriod;
int condPeriod = cond.path("period").asInt(-1);
if (condPeriod > 0) return condPeriod;
}
return effectivePeriod(periodOverride, p, lengthKey, defaultLen);
}
/** 신호선 SMA/EMA 길이 — rightPeriod·sidePeriod 우선, 없으면 지표 설정 maLength */
private int resolveSignalSmoothingPeriod(JsonNode cond, int sidePeriod,
Map<String, Object> p, String maKey, int defaultMa) {
if (sidePeriod > 0) return sidePeriod;
if (cond != null && !cond.isNull()) {
int rightPeriod = cond.path("rightPeriod").asInt(-1);
if (rightPeriod > 0) return rightPeriod;
}
return intP(p, maKey, defaultMa);
}
/** 조건 period / left·right sidePeriod 가 지표 length 계열 파라미터보다 우선 */
private int effectivePeriod(int periodOverride, Map<String, Object> p, String paramKey, int defaultVal) {
if (periodOverride > 0) return periodOverride;
return intP(p, paramKey, defaultVal);
}
private int intP(Map<String, Object> p, String k, int def) {
if (p == null) return def;
Object v = p.get(k);
@@ -1512,4 +1626,19 @@ public class StrategyDslToTa4jAdapter {
merged.putAll(overrides);
return merged;
}
/** 패키지 단위 테스트 — condPeriod/sidePeriod 가 지표에 반영되는지 검증용 */
Indicator<Num> resolveIndicatorFieldForTest(String field, String indType,
Map<String, Object> p, BarSeries s,
int condPeriod, int sidePeriod) {
return resolveIndicatorField(field, indType, p, Map.of(), s, condPeriod, sidePeriod, null);
}
/** 패키지 단위 테스트 — cond JSON 포함 CCI 본선/신호선 연동 검증용 */
Indicator<Num> resolveIndicatorFieldForTest(String field, String indType,
Map<String, Object> p, BarSeries s,
int condPeriod, int sidePeriod,
JsonNode cond) {
return resolveIndicatorField(field, indType, p, Map.of(), s, condPeriod, sidePeriod, cond);
}
}