일목균형표 수정

This commit is contained in:
Macbook
2026-05-27 23:36:48 +09:00
parent 9cee6387c3
commit 8cc0d1c88c
73 changed files with 2256 additions and 334 deletions
@@ -95,6 +95,7 @@ public class AppSettingsService {
if (d.containsKey("chartLiveReceiveHighlight")) s.setChartLiveReceiveHighlight(
Boolean.parseBoolean(d.get("chartLiveReceiveHighlight").toString()));
if (d.containsKey("chartLegendOptions")) s.setChartLegendOptionsJson(toJson(d.get("chartLegendOptions")));
if (d.containsKey("chartPaneSeparator")) s.setChartPaneSeparatorJson(toJson(d.get("chartPaneSeparator")));
if (d.containsKey("tradeAlertPopup")) s.setTradeAlertPopup(
Boolean.parseBoolean(d.get("tradeAlertPopup").toString()));
if (d.containsKey("tradeAlertSoundEnabled")) s.setTradeAlertSoundEnabled(
@@ -192,6 +193,7 @@ public class AppSettingsService {
m.put("chartVolumeVisible", s.getChartVolumeVisible() != null ? s.getChartVolumeVisible() : true);
m.put("chartLiveReceiveHighlight", s.getChartLiveReceiveHighlight() != null ? s.getChartLiveReceiveHighlight() : true);
m.put("chartLegendOptions", parseJson(s.getChartLegendOptionsJson()));
m.put("chartPaneSeparator", parseJson(s.getChartPaneSeparatorJson()));
m.put("tradeAlertPopup", s.getTradeAlertPopup() != null ? s.getTradeAlertPopup() : true);
m.put("tradeAlertSoundEnabled", s.getTradeAlertSoundEnabled() != null ? s.getTradeAlertSoundEnabled() : true);
m.put("tradeAlertSound", s.getTradeAlertSound() != null ? s.getTradeAlertSound() : "bell");
@@ -588,13 +588,13 @@ public class IndicatorService {
int conv = intP(p, "conversionPeriods", 9);
int base = intP(p, "basePeriods", 26);
int span2 = intP(p, "laggingSpan2Periods", 52);
int disp = intP(p, "displacement", 26);
int senkouDisp = intP(p, "senkouDisplacement", intP(p, "displacement", 26));
int chikouDisp = intP(p, "chikouDisplacement", intP(p, "displacement", 26));
IchimokuTenkanSenIndicator tenkan = new IchimokuTenkanSenIndicator(s, conv);
IchimokuKijunSenIndicator kijun = new IchimokuKijunSenIndicator(s, base);
IchimokuSenkouSpanAIndicator spanA = new IchimokuSenkouSpanAIndicator(s, tenkan, kijun, disp);
// 선행스팬B displacement 도 spanA·chikou 와 동일한 disp 를 적용해야 한다
IchimokuSenkouSpanBIndicator spanB = new IchimokuSenkouSpanBIndicator(s, span2, disp);
IchimokuChikouSpanIndicator chikou = new IchimokuChikouSpanIndicator(s, disp);
IchimokuSenkouSpanAIndicator spanA = new IchimokuSenkouSpanAIndicator(s, tenkan, kijun, senkouDisp);
IchimokuSenkouSpanBIndicator spanB = new IchimokuSenkouSpanBIndicator(s, span2, senkouDisp);
IchimokuChikouSpanIndicator chikou = new IchimokuChikouSpanIndicator(s, chikouDisp);
return Map.of(
"plot0", toPlot(s, tenkan),
"plot1", toPlot(s, kijun),
@@ -58,6 +58,7 @@ public class LiveStrategyTimeframeService {
if (candleType == null || candleType.isBlank()) return "1m";
String c = candleType.trim().toLowerCase();
if ("1d".equals(c)) return "1d";
if ("1w".equals(c)) return "1w";
if ("1h".equals(c)) return "1h";
if ("4h".equals(c)) return "4h";
if (ALLOWED.contains(c)) return c;
@@ -69,7 +70,8 @@ public class LiveStrategyTimeframeService {
case "1m", "3m", "5m", "10m", "15m", "30m" -> chartTf;
case "1h", "4h" -> chartTf;
case "1D" -> "1d";
case "1W", "1M" -> "1d";
case "1W" -> "1w";
case "1M" -> "1d";
default -> null;
};
}
@@ -148,7 +148,7 @@ public class StrategyConditionTimeframeService {
String type = node.path("type").asText("");
if ("TIMEFRAME".equals(type)) {
out.add(LiveStrategyTimeframeService.normalize(node.path("candleType").asText("1m")));
addTimeframeCandleTypes(node, out);
return true;
}
@@ -157,7 +157,7 @@ public class StrategyConditionTimeframeService {
if (children.isArray() && !children.isEmpty()) {
if (allTimeframeChildren(children)) {
for (JsonNode tf : children) {
out.add(LiveStrategyTimeframeService.normalize(tf.path("candleType").asText("1m")));
addTimeframeCandleTypes(tf, out);
}
return true;
}
@@ -183,4 +183,17 @@ public class StrategyConditionTimeframeService {
}
return true;
}
/** TIMEFRAME 노드 — candleTypes 배열 또는 단일 candleType */
static void addTimeframeCandleTypes(JsonNode timeframeNode, Set<String> out) {
JsonNode arr = timeframeNode.path("candleTypes");
if (arr.isArray() && !arr.isEmpty()) {
for (JsonNode t : arr) {
String ct = LiveStrategyTimeframeService.normalize(t.asText(""));
if (!ct.isBlank()) out.add(ct);
}
return;
}
out.add(LiveStrategyTimeframeService.normalize(timeframeNode.path("candleType").asText("1m")));
}
}
@@ -39,6 +39,10 @@ public class StrategyDslTimeframeNormalizer {
if (root == null || root.isNull()) return root;
if ("TIMEFRAME".equals(root.path("type").asText(""))) {
// START 다중 분봉(candleTypes) — 편집기 저장값 유지
if (hasMultipleCandleTypes(root)) {
return root;
}
String expected = inferPrimaryCandleType(root, strategyName);
String current = LiveStrategyTimeframeService.normalize(root.path("candleType").asText("1m"));
if (!expected.equals(current)) {
@@ -145,7 +149,7 @@ public class StrategyDslTimeframeNormalizer {
String type = node.path("type").asText("");
if ("TIMEFRAME".equals(type)) {
out.add(LiveStrategyTimeframeService.normalize(node.path("candleType").asText("1m")));
StrategyConditionTimeframeService.addTimeframeCandleTypes(node, out);
}
if ("CONDITION".equals(type)) {
@@ -193,6 +197,11 @@ public class StrategyDslTimeframeNormalizer {
return "1m";
}
private static boolean hasMultipleCandleTypes(JsonNode timeframeNode) {
JsonNode arr = timeframeNode.path("candleTypes");
return arr.isArray() && arr.size() > 1;
}
private ObjectNode copyTimeframeWithCandleType(JsonNode tf, String candleType) {
ObjectNode copy = tf.deepCopy();
copy.put("candleType", LiveStrategyTimeframeService.normalize(candleType));
@@ -749,17 +749,24 @@ public class StrategyDslToTa4jAdapter {
private IchimokuKijunSenIndicator ichimokuKijun(BarSeries s, Map<String, Object> p) {
return new IchimokuKijunSenIndicator(s, intP(p, "basePeriods", 26));
}
private int ichimokuSenkouDisplacement(Map<String, Object> p) {
return intP(p, "senkouDisplacement", intP(p, "displacement", 26));
}
private int ichimokuChikouDisplacement(Map<String, Object> p) {
return intP(p, "chikouDisplacement", intP(p, "displacement", 26));
}
private IchimokuSenkouSpanAIndicator ichimokuSpanA(BarSeries s, Map<String, Object> p) {
return new IchimokuSenkouSpanAIndicator(s, ichimokuTenkan(s, p), ichimokuKijun(s, p),
intP(p, "displacement", 26));
ichimokuSenkouDisplacement(p));
}
private IchimokuSenkouSpanBIndicator ichimokuSpanB(BarSeries s, Map<String, Object> p) {
// IndicatorService 와 동일하게 displacement 파라미터 반영
return new IchimokuSenkouSpanBIndicator(s, intP(p, "laggingSpan2Periods", 52),
intP(p, "displacement", 26));
ichimokuSenkouDisplacement(p));
}
private IchimokuChikouSpanIndicator ichimokuLagging(BarSeries s, Map<String, Object> p) {
return new IchimokuChikouSpanIndicator(s, intP(p, "displacement", 26));
return new IchimokuChikouSpanIndicator(s, ichimokuChikouDisplacement(p));
}
/** 종가가 구름 위 */
@@ -82,9 +82,17 @@ public class StrategyTriggerBranchEvaluator {
String type = node.path("type").asText("");
if ("TIMEFRAME".equals(type)) {
String ct = LiveStrategyTimeframeService.normalize(node.path("candleType").asText("1m"));
List<String> types = readTimeframeCandleTypes(node);
JsonNode sub = firstChild(node);
return new BranchScope("SINGLE", List.of(new BranchDef(ct, sub)));
if (types.size() <= 1) {
String ct = types.isEmpty() ? "1m" : types.get(0);
return new BranchScope("SINGLE", List.of(new BranchDef(ct, sub)));
}
List<BranchDef> branches = new ArrayList<>();
for (String ct : types) {
branches.add(new BranchDef(ct, sub));
}
return new BranchScope("OR", branches);
}
if ("AND".equals(type) || "OR".equals(type)) {
@@ -92,9 +100,10 @@ public class StrategyTriggerBranchEvaluator {
if (children.isArray() && !children.isEmpty() && allTimeframeChildren(children)) {
List<BranchDef> branches = new ArrayList<>();
for (JsonNode child : children) {
String ct = LiveStrategyTimeframeService.normalize(
child.path("candleType").asText("1m"));
branches.add(new BranchDef(ct, firstChild(child)));
JsonNode sub = firstChild(child);
for (String ct : readTimeframeCandleTypes(child)) {
branches.add(new BranchDef(ct, sub));
}
}
return new BranchScope(type, branches);
}
@@ -102,9 +111,18 @@ public class StrategyTriggerBranchEvaluator {
JsonNode wrapped = findTimeframeWrapper(node);
if (wrapped != null) {
String ct = LiveStrategyTimeframeService.normalize(wrapped.path("candleType").asText("1m"));
List<String> types = readTimeframeCandleTypes(wrapped);
JsonNode sub = firstChild(wrapped);
return new BranchScope("SINGLE", List.of(new BranchDef(ct, sub != null && !sub.isNull() ? sub : node)));
JsonNode subtree = sub != null && !sub.isNull() ? sub : node;
if (types.size() <= 1) {
String ct = types.isEmpty() ? "1m" : types.get(0);
return new BranchScope("SINGLE", List.of(new BranchDef(ct, subtree)));
}
List<BranchDef> branches = new ArrayList<>();
for (String ct : types) {
branches.add(new BranchDef(ct, subtree));
}
return new BranchScope("OR", branches);
}
String inferred = StrategyDslTimeframeNormalizer.inferPrimaryCandleType(node);
@@ -143,6 +161,21 @@ public class StrategyTriggerBranchEvaluator {
return true;
}
/** START 다중 분봉 — 각 마감봉마다 동일 조건 트리를 독립 평가 */
private static List<String> readTimeframeCandleTypes(JsonNode timeframeNode) {
JsonNode arr = timeframeNode.path("candleTypes");
if (arr.isArray() && !arr.isEmpty()) {
List<String> list = new ArrayList<>();
for (JsonNode t : arr) {
String ct = LiveStrategyTimeframeService.normalize(t.asText(""));
if (!ct.isBlank() && !list.contains(ct)) list.add(ct);
}
if (!list.isEmpty()) return list;
}
return List.of(LiveStrategyTimeframeService.normalize(
timeframeNode.path("candleType").asText("1m")));
}
private final class TriggerBranchRule implements Rule {
private final BranchScope scope;
private final String market;