전략편집기 수정

This commit is contained in:
Macbook
2026-05-25 17:56:25 +09:00
parent aac6454724
commit 02d14e4b2b
39 changed files with 1974 additions and 288 deletions
@@ -135,7 +135,7 @@ public class StrategyDslToTa4jAdapter {
case "OR" -> buildOrRule(node, ctx);
case "NOT" -> buildNotRule(node, ctx);
case "TIMEFRAME" -> buildTimeframeRule(node, ctx);
default -> buildConditionRule(node.path("condition"), ctx.primarySeries(), ctx.indicatorParams());
default -> buildConditionRule(node.path("condition"), ctx);
};
}
@@ -261,10 +261,12 @@ public class StrategyDslToTa4jAdapter {
// ── CONDITION ─────────────────────────────────────────────────────────────
private Rule buildConditionRule(JsonNode cond, BarSeries series,
Map<String, Map<String, Object>> params) {
private Rule buildConditionRule(JsonNode cond, RuleBuildContext ctx) {
if (cond == null || cond.isNull()) return new BooleanRule(false);
BarSeries series = ctx.primarySeries();
Map<String, Map<String, Object>> params = ctx.indicatorParams();
String indType = cond.path("indicatorType").asText("");
String condType = cond.path("conditionType").asText("GT");
String leftField = cond.path("leftField").asText("NONE");
@@ -275,6 +277,10 @@ public class StrategyDslToTa4jAdapter {
int slopePeriod = cond.path("slopePeriod").asInt(3);
int holdDays = cond.path("holdDays").asInt(3);
if (needsCrossTimeframeComposite(cond, ctx)) {
return buildCrossTimeframeCompositeRule(cond, ctx);
}
// DSL 타입(STOCHASTIC 등) → DB 레지스트리 키(Stochastic 등) 변환 후 파라미터 조회
String registryKey = toRegistryKey(indType);
Map<String, Object> indParams = params != null
@@ -336,6 +342,104 @@ public class StrategyDslToTa4jAdapter {
}
}
/** 복합지표 — leftCandleType / rightCandleType 이 서로 다를 때 교차 시간봉 평가 */
private boolean needsCrossTimeframeComposite(JsonNode cond, RuleBuildContext ctx) {
if (!cond.path("composite").asBoolean(false)) return false;
if (ctx.storage() == null || ctx.market() == null || ctx.market().isBlank()) return false;
String leftCt = LiveStrategyTimeframeService.normalize(
cond.path("leftCandleType").asText("1m"));
String rightCt = LiveStrategyTimeframeService.normalize(
cond.path("rightCandleType").asText("1m"));
return !leftCt.equals(rightCt);
}
private Rule buildCrossTimeframeCompositeRule(JsonNode cond, RuleBuildContext ctx) {
BarSeries trigger = ctx.primarySeries();
String indType = cond.path("indicatorType").asText("");
String condType = cond.path("conditionType").asText("GT");
String leftField = cond.path("leftField").asText("NONE");
String rightField = cond.path("rightField").asText("NONE");
int condPeriod = cond.path("period").asInt(-1);
int leftPeriod = cond.path("leftPeriod").asInt(-1);
int rightPeriod = cond.path("rightPeriod").asInt(-1);
String registryKey = toRegistryKey(indType);
Map<String, Object> indParams = ctx.indicatorParams() != null
? ctx.indicatorParams().getOrDefault(registryKey, Map.of())
: Map.of();
String leftCt = LiveStrategyTimeframeService.normalize(
cond.path("leftCandleType").asText("1m"));
String rightCt = LiveStrategyTimeframeService.normalize(
cond.path("rightCandleType").asText("1m"));
BarSeries leftSeries = ctx.resolveSeries(leftCt);
BarSeries rightSeries = ctx.resolveSeries(rightCt);
try {
Indicator<Num> left = resolveField(leftField, indType, indParams, leftSeries, condPeriod, leftPeriod);
Indicator<Num> right = resolveField(rightField, indType, indParams, rightSeries, condPeriod, rightPeriod);
return new CrossTimeframeCompositeRule(condType, left, right, trigger, leftSeries, rightSeries);
} catch (Exception e) {
log.warn("[Adapter] 복합 교차시간봉 빌드 실패 ind={} cond={}: {}",
indType, condType, e.getMessage());
return new BooleanRule(false);
}
}
private static int evalIndex(BarSeries trigger, BarSeries branch, int triggerIndex) {
if (trigger == branch) return triggerIndex;
int end = branch.getEndIndex();
return end >= 0 ? end : 0;
}
private static final class CrossTimeframeCompositeRule implements Rule {
private final String condType;
private final Indicator<Num> left;
private final Indicator<Num> right;
private final BarSeries triggerSeries;
private final BarSeries leftSeries;
private final BarSeries rightSeries;
CrossTimeframeCompositeRule(String condType,
Indicator<Num> left,
Indicator<Num> right,
BarSeries triggerSeries,
BarSeries leftSeries,
BarSeries rightSeries) {
this.condType = condType;
this.left = left;
this.right = right;
this.triggerSeries = triggerSeries;
this.leftSeries = leftSeries;
this.rightSeries = rightSeries;
}
@Override
public boolean isSatisfied(int index, TradingRecord tradingRecord) {
int li = evalIndex(triggerSeries, leftSeries, index);
int ri = evalIndex(triggerSeries, rightSeries, index);
if (li < 1 || ri < 1) return false;
Num l0 = left.getValue(li - 1);
Num l1 = left.getValue(li);
Num r0 = right.getValue(ri - 1);
Num r1 = right.getValue(ri);
if (l0 == null || l1 == null || r0 == null || r1 == null) return false;
return switch (condType) {
case "GT" -> l1.isGreaterThan(r1);
case "LT" -> l1.isLessThan(r1);
case "GTE" -> l1.isGreaterThan(r1) || l1.isEqual(r1);
case "LTE" -> l1.isLessThan(r1) || l1.isEqual(r1);
case "EQ" -> l1.isEqual(r1);
case "NEQ" -> !l1.isEqual(r1);
case "CROSS_UP" -> l0.isLessThanOrEqual(r0) && l1.isGreaterThan(r1);
case "CROSS_DOWN" -> l0.isGreaterThanOrEqual(r0) && l1.isLessThan(r1);
default -> false;
};
}
}
// ── 필드 Resolver ─────────────────────────────────────────────────────────
private Indicator<Num> resolveField(String field, String indType,