다중 시간봉 전략 설정기능 추가

This commit is contained in:
Macbook
2026-05-25 02:49:39 +09:00
parent cc01f311bc
commit 30dedc4abc
17 changed files with 1903 additions and 249 deletions
@@ -1,6 +1,7 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.goldenchart.storage.Ta4jStorage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.ta4j.core.BarSeries;
@@ -98,56 +99,133 @@ public class StrategyDslToTa4jAdapter {
// ── Public API ────────────────────────────────────────────────────────────
public record RuleBuildContext(
BarSeries primarySeries,
Map<String, Map<String, Object>> indicatorParams,
String market,
Ta4jStorage storage
) {
BarSeries resolveSeries(String candleType) {
String ct = LiveStrategyTimeframeService.normalize(candleType);
if (storage != null && market != null && !market.isBlank()
&& storage.exists(market, ct)) {
return storage.getOrCreate(market, ct);
}
return primarySeries;
}
}
public Rule toRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> indicatorParams) {
return toRule(node, new RuleBuildContext(series, indicatorParams, null, null));
}
public Rule toRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> indicatorParams,
String market, Ta4jStorage storage) {
return toRule(node, new RuleBuildContext(series, indicatorParams, market, storage));
}
public Rule toRule(JsonNode node, RuleBuildContext ctx) {
if (node == null || node.isNull()) return new BooleanRule(false);
String type = node.path("type").asText("CONDITION");
return switch (type) {
case "AND" -> buildAndRule(node, series, indicatorParams);
case "OR" -> buildOrRule(node, series, indicatorParams);
case "NOT" -> buildNotRule(node, series, indicatorParams);
default -> buildConditionRule(node.path("condition"), series, indicatorParams);
case "AND" -> buildAndRule(node, ctx);
case "OR" -> buildOrRule(node, ctx);
case "NOT" -> buildNotRule(node, ctx);
case "TIMEFRAME" -> buildTimeframeRule(node, ctx);
default -> buildConditionRule(node.path("condition"), ctx.primarySeries(), ctx.indicatorParams());
};
}
private Rule buildTimeframeRule(JsonNode node, RuleBuildContext ctx) {
String ct = node.path("candleType").asText("1m");
BarSeries branchSeries = ctx.resolveSeries(ct);
RuleBuildContext branchCtx = new RuleBuildContext(
branchSeries, ctx.indicatorParams(), ctx.market(), ctx.storage());
List<Rule> rules = childRules(node, branchCtx);
if (rules.isEmpty()) return new BooleanRule(false);
Rule inner = rules.get(0);
if (branchSeries == ctx.primarySeries()) return inner;
return new CrossSeriesRule(inner, branchSeries, ctx.primarySeries());
}
/** 다른 시간봉 시리즈로 빌드된 Rule — 트리거 시리즈 인덱스에서 해당 분봉 최신봉으로 평가 */
private static final class CrossSeriesRule implements Rule {
private final Rule inner;
private final BarSeries branchSeries;
private final BarSeries triggerSeries;
CrossSeriesRule(Rule inner, BarSeries branchSeries, BarSeries triggerSeries) {
this.inner = inner;
this.branchSeries = branchSeries;
this.triggerSeries = triggerSeries;
}
@Override
public boolean isSatisfied(int index, TradingRecord tradingRecord) {
int evalIndex = branchSeries == triggerSeries
? index
: branchSeries.getEndIndex();
if (evalIndex < 0 || branchSeries.getBarCount() == 0) return false;
return inner.isSatisfied(evalIndex, tradingRecord);
}
}
// ── AND / OR / NOT ────────────────────────────────────────────────────────
private Rule buildAndRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> p) {
List<Rule> rules = childRules(node, series, p);
private Rule buildAndRule(JsonNode node, RuleBuildContext ctx) {
List<Rule> rules = childRules(node, ctx);
if (rules.isEmpty()) return new BooleanRule(false);
Rule result = rules.get(0);
for (int i = 1; i < rules.size(); i++) result = new AndRule(result, rules.get(i));
return result;
}
private Rule buildOrRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> p) {
List<Rule> rules = childRules(node, series, p);
private Rule buildOrRule(JsonNode node, RuleBuildContext ctx) {
List<Rule> rules = childRules(node, ctx);
if (rules.isEmpty()) return new BooleanRule(false);
Rule result = rules.get(0);
for (int i = 1; i < rules.size(); i++) result = new OrRule(result, rules.get(i));
return result;
}
private Rule buildNotRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> p) {
List<Rule> rules = childRules(node, series, p);
private Rule buildNotRule(JsonNode node, RuleBuildContext ctx) {
List<Rule> rules = childRules(node, ctx);
if (rules.isEmpty()) return new BooleanRule(false);
return new NotRule(rules.get(0));
}
private List<Rule> childRules(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> p) {
private List<Rule> childRules(JsonNode node, RuleBuildContext ctx) {
List<Rule> list = new ArrayList<>();
JsonNode children = node.path("children");
if (children.isArray()) {
for (JsonNode child : children) list.add(toRule(child, series, p));
for (JsonNode child : children) list.add(toRule(child, ctx));
}
return list;
}
private Rule buildAndRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> p) {
return buildAndRule(node, new RuleBuildContext(series, p, null, null));
}
private Rule buildOrRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> p) {
return buildOrRule(node, new RuleBuildContext(series, p, null, null));
}
private Rule buildNotRule(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> p) {
return buildNotRule(node, new RuleBuildContext(series, p, null, null));
}
private List<Rule> childRules(JsonNode node, BarSeries series,
Map<String, Map<String, Object>> p) {
return childRules(node, new RuleBuildContext(series, p, null, null));
}
// ── CONDITION ─────────────────────────────────────────────────────────────
private Rule buildConditionRule(JsonNode cond, BarSeries series,