전략평가 MA 참조에러 수정

This commit is contained in:
Macbook
2026-06-13 01:25:56 +09:00
parent 24128e6a0a
commit 207374a08e
10 changed files with 451 additions and 77 deletions
@@ -500,7 +500,7 @@ public class LiveConditionStatusService {
.displayName(indType)
.conditionType(condType)
.conditionLabel(condLabel)
.thresholdLabel(formatThresholdStatic(c, target))
.thresholdLabel(formatThresholdStatic(c, target, visual))
.currentValue(null)
.targetValue(target)
.satisfied(null)
@@ -510,7 +510,8 @@ public class LiveConditionStatusService {
.build();
}
private String formatThresholdStatic(JsonNode cond, Double target) {
private String formatThresholdStatic(JsonNode cond, Double target,
Map<String, Map<String, Object>> params) {
if (target != null && !target.isNaN()) {
String ct = cond.path("conditionType").asText("GT");
String v = formatNum(target);
@@ -525,7 +526,7 @@ public class LiveConditionStatusService {
}
String rf = cond.path("rightField").asText("");
if (rf != null && !rf.isBlank() && !"NONE".equals(rf)) {
return rf.replace('_', ' ');
return adapter.formatMovingAverageFieldLabel(rf, params);
}
return "";
}
@@ -576,16 +577,17 @@ public class LiveConditionStatusService {
Double rightLive = adapter.readConditionFieldValue(cond, series, params, index, false);
if (rightLive != null && !rightLive.isNaN()) {
String v = formatNum(rightLive);
String fieldLabel = adapter.formatMovingAverageFieldLabel(rf, params);
return switch (ct) {
case "LT", "CROSS_DOWN" -> "< " + v;
case "GT", "CROSS_UP" -> "> " + v;
case "GTE" -> "" + v;
case "LTE" -> "" + v;
case "EQ" -> "= " + v;
default -> rf.replace('_', ' ') + " (" + v + ")";
default -> fieldLabel + " (" + v + ")";
};
}
return rf.replace('_', ' ');
return adapter.formatMovingAverageFieldLabel(rf, params);
}
return "";
}
@@ -138,6 +138,13 @@ public class StrategyDslToTa4jAdapter {
market, storage, useConfirmedOnly, seriesOverrides);
}
/** MA1~11 슬롯 → period1~11 (SMA 보조지표 설정) */
public Map<String, Object> smaParams() {
if (indicatorParams == null) return Map.of();
Map<String, Object> sma = indicatorParams.get("SMA");
return sma != null ? sma : Map.of();
}
/** 사용 중인 시리즈 오버라이드가 있으면 백테스트 모드 */
public boolean isBacktest() {
return seriesOverrides != null && !seriesOverrides.isEmpty();
@@ -233,7 +240,9 @@ public class StrategyDslToTa4jAdapter {
: Map.of();
try {
Indicator<Num> ind = resolveField(field, indType, indParams, series, condPeriod, sidePeriod);
RuleBuildContext ctx = new RuleBuildContext(series, params != null ? params : Map.of(),
Map.of(), null, null, false, Map.of());
Indicator<Num> ind = resolveField(field, indType, indParams, series, condPeriod, sidePeriod, cond, ctx);
Num v = ind.getValue(index);
return v != null ? v.doubleValue() : null;
} catch (Exception e) {
@@ -722,7 +731,7 @@ public class StrategyDslToTa4jAdapter {
case "VOLUME_VALUE" -> new VolumeIndicator(s, 1);
default -> {
Double hlineVal = IndicatorHlineResolver.resolveThresholdField(
cond, field, indType, ctx.indicatorVisual());
cond, field, indType, ctx != null ? ctx.indicatorVisual() : Map.of());
if (hlineVal != null) {
yield new ConstantIndicator<>(s, s.numFactory().numOf(hlineVal));
}
@@ -730,7 +739,8 @@ public class StrategyDslToTa4jAdapter {
if (!Double.isNaN(constant)) {
yield new ConstantIndicator<>(s, s.numFactory().numOf(constant));
}
yield resolveIndicatorField(field, indType, p, s, condPeriod, sidePeriod);
Map<String, Object> sma = ctx != null ? ctx.smaParams() : Map.of();
yield resolveIndicatorField(field, indType, p, sma, s, condPeriod, sidePeriod);
}
};
}
@@ -764,7 +774,9 @@ public class StrategyDslToTa4jAdapter {
}
private Indicator<Num> resolveIndicatorField(String field, String indType,
Map<String, Object> p, BarSeries s,
Map<String, Object> p,
Map<String, Object> smaParams,
BarSeries s,
int condPeriod, int sidePeriod) {
ClosePriceIndicator close = new ClosePriceIndicator(s);
int periodOverride = sidePeriod > 0 ? sidePeriod : (condPeriod > 0 ? condPeriod : -1);
@@ -943,12 +955,12 @@ public class StrategyDslToTa4jAdapter {
return new LowestValueIndicator(close, period);
}
if (field.startsWith("MA") && !field.startsWith("MACD")) {
try { return new SMAIndicator(close, Integer.parseInt(field.substring(2))); }
catch (NumberFormatException ignored) { /* fall */ }
int period = resolveMovingAveragePeriod(field, "MA", smaParams);
if (period > 0) return new SMAIndicator(close, period);
}
if (field.startsWith("EMA")) {
try { return new EMAIndicator(close, Integer.parseInt(field.substring(3))); }
catch (NumberFormatException ignored) { /* fall */ }
int period = resolveMovingAveragePeriod(field, "EMA", smaParams);
if (period > 0) return new EMAIndicator(close, period);
}
// 일목균형표
if (field.equals("CONVERSION_LINE")) return ichimokuTenkan(s, p);
@@ -1304,6 +1316,61 @@ public class StrategyDslToTa4jAdapter {
return new PreviousValueIndicator(lowest, 1);
}
// ── MA/EMA DSL (MA1~11 ↔ SMA period1~11) ─────────────────────────────────
/** SMA 차트 MA1~MA11 기본 기간 — IndicatorService 와 동일 */
private static final int[] SMA_DEFAULT_PERIODS =
{ 5, 12, 16, 20, 26, 34, 50, 60, 72, 90, 120 };
private static final int MA_DSL_SLOT_MAX = 11;
/**
* MA/EMA DSL 필드 → SMA/EMA 계산 기간.
* MA3·MA5(1~11): SMA 설정 period1~11. MA20·MA60(&gt;11): 레거시 기간 인코딩.
*/
public int resolveMovingAveragePeriod(String field, String prefix, Map<String, Object> smaParams) {
if (field == null || !field.startsWith(prefix)) return -1;
if ("MA".equals(prefix) && field.startsWith("MACD")) return -1;
try {
int n = Integer.parseInt(field.substring(prefix.length()));
if (n >= 1 && n <= MA_DSL_SLOT_MAX) {
String periodKey = "period" + n;
int def = SMA_DEFAULT_PERIODS[n - 1];
return Math.max(1, intP(smaParams, periodKey, def));
}
if (n > MA_DSL_SLOT_MAX) return n;
} catch (NumberFormatException ignored) { /* fall */ }
return -1;
}
/** live-conditions·해석 UI — MA5(60일) 형식 */
public String formatMovingAverageFieldLabel(String field, Map<String, Map<String, Object>> params) {
if (field == null || field.isBlank()) return field;
Map<String, Object> sma = params != null ? params.getOrDefault("SMA", Map.of()) : Map.of();
if (field.startsWith("MA") && !field.startsWith("MACD")) {
int period = resolveMovingAveragePeriod(field, "MA", sma);
if (period <= 0) return field;
try {
int n = Integer.parseInt(field.substring(2));
if (n >= 1 && n <= MA_DSL_SLOT_MAX) return "MA" + n + "(" + period + "일)";
return "MA(" + period + "일)";
} catch (NumberFormatException e) {
return field;
}
}
if (field.startsWith("EMA")) {
int period = resolveMovingAveragePeriod(field, "EMA", sma);
if (period <= 0) return field;
try {
int n = Integer.parseInt(field.substring(3));
if (n >= 1 && n <= MA_DSL_SLOT_MAX) return "EMA" + n + "(" + period + "일)";
return "EMA(" + period + "일)";
} catch (NumberFormatException e) {
return field;
}
}
return field;
}
// ── 파라미터 헬퍼 ─────────────────────────────────────────────────────────
private int intP(Map<String, Object> p, String k, int def) {
@@ -0,0 +1,59 @@
package com.goldenchart.service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* MA1~MA11 슬롯 ↔ SMA period1~11 매핑 및 레거시 MA20·MA60(기간 인코딩) 호환.
*/
class MaDslFieldAdapterTest {
private StrategyDslToTa4jAdapter adapter;
@BeforeEach
void setUp() {
adapter = new StrategyDslToTa4jAdapter();
}
@Test
void resolveMovingAveragePeriod_slotUsesSmaParams() {
Map<String, Object> sma = Map.of("period3", 20, "period5", 60);
assertEquals(20, adapter.resolveMovingAveragePeriod("MA3", "MA", sma));
assertEquals(60, adapter.resolveMovingAveragePeriod("MA5", "MA", sma));
}
@Test
void resolveMovingAveragePeriod_legacyPeriodEncoding() {
Map<String, Object> sma = Map.of("period3", 20, "period5", 60);
assertEquals(20, adapter.resolveMovingAveragePeriod("MA20", "MA", sma));
assertEquals(60, adapter.resolveMovingAveragePeriod("MA60", "MA", sma));
}
@Test
void resolveMovingAveragePeriod_oldSlotBugWouldUseLiteralDays() {
Map<String, Object> sma = Map.of("period3", 20, "period5", 60);
// MA3 must NOT resolve to 3-day SMA when period3=20
assertEquals(20, adapter.resolveMovingAveragePeriod("MA3", "MA", sma));
assertEquals(60, adapter.resolveMovingAveragePeriod("MA5", "MA", sma));
}
@Test
void formatMovingAverageFieldLabel_slotLabels() {
Map<String, Map<String, Object>> params = Map.of(
"SMA", Map.of("period3", 20, "period5", 60));
assertEquals("MA3(20일)", adapter.formatMovingAverageFieldLabel("MA3", params));
assertEquals("MA5(60일)", adapter.formatMovingAverageFieldLabel("MA5", params));
}
@Test
void formatMovingAverageFieldLabel_legacyLabels() {
Map<String, Map<String, Object>> params = Map.of(
"SMA", Map.of("period3", 20, "period5", 60));
assertEquals("MA(20일)", adapter.formatMovingAverageFieldLabel("MA20", params));
assertEquals("MA(60일)", adapter.formatMovingAverageFieldLabel("MA60", params));
}
}