가상투자 종목카드박스 레이아웃 수정

This commit is contained in:
Macbook
2026-05-26 16:42:57 +09:00
parent 7cf43609dc
commit c0d21ac825
25 changed files with 1632 additions and 61 deletions
@@ -86,6 +86,8 @@ public class StrategyDslToTa4jAdapter {
Map.entry("VR", "VR"),
Map.entry("DISPARITY", "Disparity"),
Map.entry("DONCHIAN", "DonchianChannels"),
Map.entry("NEW_HIGH", "PriceExtreme"),
Map.entry("NEW_LOW", "PriceExtreme"),
Map.entry("VOLUME", "VOLUME")
);
@@ -281,6 +283,15 @@ public class StrategyDslToTa4jAdapter {
return buildCrossTimeframeCompositeRule(cond, ctx);
}
PriceExtremeNorm px = normalizePriceExtremeCondition(cond);
if (px != null) {
indType = px.indType();
condType = px.condType();
leftField = px.leftField();
rightField = px.rightField();
condPeriod = px.period();
}
// DSL 타입(STOCHASTIC 등) → DB 레지스트리 키(Stochastic 등) 변환 후 파라미터 조회
String registryKey = toRegistryKey(indType);
Map<String, Object> indParams = params != null
@@ -291,7 +302,7 @@ public class StrategyDslToTa4jAdapter {
Indicator<Num> left = resolveField(leftField, indType, indParams, series, condPeriod, leftPeriod);
Indicator<Num> right = resolveField(rightField, indType, indParams, series, condPeriod, rightPeriod);
return switch (condType) {
Rule core = switch (condType) {
case "GT" -> new OverIndicatorRule(left, right);
case "LT" -> new UnderIndicatorRule(left, right);
// GTE/LTE: OverIndicator OR 동일 (epsilon 비교)
@@ -336,12 +347,72 @@ public class StrategyDslToTa4jAdapter {
yield new BooleanRule(false);
}
};
if (px != null) {
return nanSafeCompareRule(core, left, right);
}
return core;
} catch (Exception e) {
log.warn("[Adapter] 조건 빌드 실패 ind={} cond={}: {}", indType, condType, e.getMessage());
return new BooleanRule(false);
}
}
/**
* 신고가·신저가 DSL 정규화 — 구버전 DC_UPPER/DC_LOWER·복합지표 승격 필드를 NH_PRIOR/NL_PRIOR 로 통일.
*/
private record PriceExtremeNorm(String indType, String condType, String leftField, String rightField, int period) {}
private PriceExtremeNorm normalizePriceExtremeCondition(JsonNode cond) {
String ind = cond.path("indicatorType").asText("");
if (!"NEW_HIGH".equals(ind) && !"NEW_LOW".equals(ind)) return null;
String ct = cond.path("conditionType").asText("GT");
String left = cond.path("leftField").asText("CLOSE_PRICE");
String right = cond.path("rightField").asText("");
int period = cond.path("period").asInt(-1);
int leftP = cond.path("leftPeriod").asInt(-1);
if (period <= 0) period = leftP > 0 ? leftP : 9;
if (right.startsWith("DC_UPPER_")) {
period = parseTrailingInt(right, "DC_UPPER_", period);
right = "NH_PRIOR_" + period;
} else if (right.startsWith("DC_LOWER_")) {
period = parseTrailingInt(right, "DC_LOWER_", period);
right = "NL_PRIOR_" + period;
} else if (right.startsWith("NH_PRIOR_")) {
period = parseTrailingInt(right, "NH_PRIOR_", period);
} else if (right.startsWith("NL_PRIOR_")) {
period = parseTrailingInt(right, "NL_PRIOR_", period);
} else if (right.isBlank() || "NONE".equals(right)) {
right = "NEW_HIGH".equals(ind) ? "NH_PRIOR_" + period : "NL_PRIOR_" + period;
}
if (left.isBlank() || "NONE".equals(left)) left = "CLOSE_PRICE";
// Donchian 당일봉 포함 DC_* + CROSS 는 거의 성립하지 않음 → PRIOR 필드 + GTE/LTE
if ("NEW_HIGH".equals(ind) && "CROSS_UP".equals(ct) && !right.startsWith("NH_PRIOR_")) {
ct = "GTE";
} else if ("NEW_LOW".equals(ind) && "CROSS_DOWN".equals(ct) && !right.startsWith("NL_PRIOR_")) {
ct = "LTE";
}
return new PriceExtremeNorm(ind, ct, left, right, period);
}
/** 직전 N봉 극값이 NaN 인 구간(워밍업)에서는 조건 미충족 */
private Rule nanSafeCompareRule(Rule inner, Indicator<Num> left, Indicator<Num> right) {
return new Rule() {
@Override
public boolean isSatisfied(int index, TradingRecord tradingRecord) {
Num l = left.getValue(index);
Num r = right.getValue(index);
if (l == null || r == null || l.isNaN() || r.isNaN()) return false;
return inner.isSatisfied(index, tradingRecord);
}
};
}
/** 복합지표 — leftCandleType / rightCandleType 이 서로 다를 때 교차 시간봉 평가 */
private boolean needsCrossTimeframeComposite(JsonNode cond, RuleBuildContext ctx) {
if (!cond.path("composite").asBoolean(false)) return false;
@@ -611,6 +682,15 @@ public class StrategyDslToTa4jAdapter {
return new WilliamsRIndicator(s, period);
}
if (field.equals("WILLIAMS_R_VALUE")) return new WilliamsRIndicator(s, intP(p, "length", 14));
// 신고가·신저가 — 직전 N봉 최고/최저 (당일 봉 제외, PreviousValueIndicator)
if (field.startsWith("NH_PRIOR_")) {
int period = parseTrailingInt(field, "NH_PRIOR_", intP(p, "length", 9));
return priorHighestHigh(s, period);
}
if (field.startsWith("NL_PRIOR_")) {
int period = parseTrailingInt(field, "NL_PRIOR_", intP(p, "length", 9));
return priorLowestLow(s, period);
}
// Donchian Channel — DC_UPPER_20 / DC_LOWER_10 등 기간 접미사
if (field.startsWith("DC_UPPER_")) {
int period = parseTrailingInt(field, "DC_UPPER_", intP(p, "length", 20));
@@ -857,6 +937,25 @@ public class StrategyDslToTa4jAdapter {
}
}
/**
* 직전 N봉 고가 중 최고값 (현재 봉 제외).
* index t 에서 max(high[t-N]..high[t-1]).
*/
private Indicator<Num> priorHighestHigh(BarSeries s, int period) {
HighPriceIndicator high = new HighPriceIndicator(s);
HighestValueIndicator highest = new HighestValueIndicator(high, period);
return new PreviousValueIndicator(highest, 1);
}
/**
* 직전 N봉 저가 중 최저값 (현재 봉 제외).
*/
private Indicator<Num> priorLowestLow(BarSeries s, int period) {
LowPriceIndicator low = new LowPriceIndicator(s);
LowestValueIndicator lowest = new LowestValueIndicator(low, period);
return new PreviousValueIndicator(lowest, 1);
}
// ── 파라미터 헬퍼 ─────────────────────────────────────────────────────────
private int intP(Map<String, Object> p, String k, int def) {